forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreadonly_state_agent_context.py
More file actions
115 lines (93 loc) · 3.83 KB
/
Copy pathreadonly_state_agent_context.py
File metadata and controls
115 lines (93 loc) · 3.83 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
"""readonly-state-agent-context — minimal MAF agent for `useAgentContext`.
Mirrors LangGraph's
`langgraph-python/src/agents/readonly_state_agent_context.py`. Demonstrates
the `useAgentContext` hook from `@copilotkit/react-core/v2`: the frontend
provides read-only context *to* the agent (e.g. user name, timezone,
recent activity). The agent reads that context on every turn and
incorporates it into its response. No custom state, no tools — the
minimal shape of the useAgentContext pattern.
"""
from __future__ import annotations
from collections.abc import AsyncGenerator
from textwrap import dedent
from typing import Any
from ag_ui.core import BaseEvent
from agent_framework import Agent, BaseChatClient
from agent_framework_ag_ui import AgentFrameworkAgent
SYSTEM_PROMPT = dedent(
"""
You are a helpful, concise assistant. The frontend may provide
read-only context about the user (e.g. name, timezone, recent
activity) via the `useAgentContext` hook. Always consult that
context when it is relevant — address the user by name if known,
respect their timezone when mentioning times, and reference recent
activity when it helps you answer. Keep responses short.
"""
).strip()
def build_context_system_message(context: Any) -> str | None:
"""Format frontend-provided AG-UI context as a model-visible message."""
if not isinstance(context, list) or len(context) == 0:
return None
lines: list[str] = ["## Context from the application"]
for entry in context:
if not isinstance(entry, dict):
continue
description = entry.get("description")
value = entry.get("value")
if description is None or value is None:
continue
lines.append("")
lines.append(str(description))
lines.append(str(value))
if len(lines) == 1:
return None
return "\n".join(lines)
class ReadonlyContextFrameworkAgent(AgentFrameworkAgent):
"""AgentFrameworkAgent that forwards `useAgentContext` to the model.
LangGraph gets this behavior from CopilotKitMiddleware. The MS Agent
adapter receives the AG-UI `context` entries in `input_data`, so this
shim appends them to the wrapped agent's instruction string before
delegating to the standard Agent Framework runner.
"""
async def run( # type: ignore[override]
self,
input_data: dict[str, Any],
) -> AsyncGenerator[BaseEvent, None]:
context_prompt = build_context_system_message(input_data.get("context"))
if not context_prompt:
async for event in super().run(input_data):
yield event
return
options = getattr(self.agent, "default_options", None)
if not isinstance(options, dict):
async for event in super().run(input_data):
yield event
return
previous_instructions = options.get("instructions")
options["instructions"] = f"{SYSTEM_PROMPT}\n\n{context_prompt}"
try:
async for event in super().run(input_data):
yield event
finally:
if previous_instructions is None:
options.pop("instructions", None)
else:
options["instructions"] = previous_instructions
def create_readonly_state_agent_context(
chat_client: BaseChatClient,
) -> ReadonlyContextFrameworkAgent:
"""Instantiate the readonly-state-agent-context MAF agent."""
base_agent = Agent(
client=chat_client,
name="readonly_state_agent_context",
instructions=SYSTEM_PROMPT,
tools=[],
)
return ReadonlyContextFrameworkAgent(
agent=base_agent,
name="ReadOnlyStateAgentContext",
description=(
"Reads frontend-provided `useAgentContext` entries on every "
"turn; no tools, no custom state."
),
)