forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshared_state_read_write.py
More file actions
122 lines (99 loc) · 4.57 KB
/
Copy pathshared_state_read_write.py
File metadata and controls
122 lines (99 loc) · 4.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""Agno agent backing the Shared State (Read + Write) demo.
Mirrors `langgraph-python/src/agents/shared_state_read_write.py` and
`google-adk/src/agents/shared_state_read_write_agent.py`.
Demonstrates the canonical bidirectional shared-state pattern between UI
and agent:
- **UI -> agent (write)**: The UI owns a `preferences` object that it
writes into agent state via `agent.setState(...)`. The Agno agent's
dynamic instructions function reads `session_state["preferences"]`
every turn and prepends a preferences block so the LLM adapts.
- **agent -> UI (read)**: The agent calls the `set_notes` tool to
replace `session_state["notes"]`. Our custom AGUI router (see
`agent_server.py`) emits a `StateSnapshotEvent` after every run so the
UI's `useAgent({ updates: [OnStateChanged] })` reflects the change.
Together this matches the canonical bidirectional shared-state contract
the langgraph-python and google-adk references implement.
"""
from __future__ import annotations
from textwrap import dedent
from typing import Any
import dotenv
from agno.agent.agent import Agent
from agno.models.openai import OpenAIChat
from agno.run import RunContext
dotenv.load_dotenv()
PREFS_BLOCK_HEADER = "[shared-state-read-write] preferences:"
def _format_preferences(prefs: Any) -> str:
"""Build the preferences block injected into the system prompt."""
if not isinstance(prefs, dict) or not prefs:
return ""
lines = [PREFS_BLOCK_HEADER]
if prefs.get("name"):
lines.append(f"- Name: {prefs['name']}")
if prefs.get("tone"):
lines.append(f"- Preferred tone: {prefs['tone']}")
if prefs.get("language"):
lines.append(f"- Preferred language: {prefs['language']}")
interests = prefs.get("interests") or []
if interests:
lines.append(f"- Interests: {', '.join(interests)}")
if len(lines) == 1:
# Truthy dict but no recognized keys — emit nothing rather than a
# bare header. Mirrors the same guard used in google-adk's
# shared_state_read_write_agent._build_prefs_block.
return ""
lines.append(
"Tailor every response to these preferences. "
"Address the user by name when appropriate."
)
return "\n".join(lines)
def build_instructions(run_context: RunContext) -> str:
"""Dynamic instructions: read latest preferences from session_state.
Agno re-evaluates this function on every run when `cache_callables`
is False, so writes the UI makes via `agent.setState({preferences})`
take effect on the very next turn.
"""
base = dedent(
"""
You are a helpful, concise assistant. The user's preferences are
supplied via shared state and added as a system message at the start
of every turn — always respect them.
When the user asks you to remember something, or you observe
something worth surfacing in the UI's notes panel, call `set_notes`
with the FULL updated list of short note strings (existing notes +
any new ones). Keep each note under 120 characters. Always pass the
complete list — `set_notes` REPLACES the notes array, it does not
append.
"""
).strip()
prefs_block = _format_preferences(getattr(run_context, "session_state", None) or {})
if prefs_block:
return f"{prefs_block}\n\n{base}"
return base
def set_notes(run_context: RunContext, notes: list[str]) -> str:
"""Replace the notes array in shared state with the full updated list.
Always pass the FULL list of short note strings (existing notes + new),
not a diff. Keep each note short (< 120 chars).
"""
if run_context.session_state is None:
run_context.session_state = {}
# Coerce all entries to plain strings — tolerate models that pass
# through stray dict/None entries from earlier turns rather than
# crash the AGUI router with a serialization failure mid-stream.
cleaned = [str(n) for n in (notes or []) if n is not None]
run_context.session_state["notes"] = cleaned
return f"Notes updated. ({len(cleaned)} total)"
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini", timeout=120),
tools=[set_notes],
# Re-evaluate instructions on every run so UI writes to
# session_state["preferences"] are visible to the LLM on the very
# next turn (rather than being cached at agent construction time).
cache_callables=False,
instructions=build_instructions,
description=(
"You adapt your responses to the user's stored preferences and use "
"the set_notes tool to surface things worth remembering."
),
tool_call_limit=5,
)