forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrender_mode.py
More file actions
177 lines (141 loc) · 6.79 KB
/
Copy pathrender_mode.py
File metadata and controls
177 lines (141 loc) · 6.79 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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Render-mode middleware for context-driven GenUI strategy switching.
Reads ``render_mode`` and ``output_schema`` from the CopilotKit context list
and adapts agent output accordingly:
- **tool-based**: no changes (default)
- **a2ui**: no changes (agent decides when to call generate_a2ui tool)
- **json-render**: append JSONL instruction to system prompt
- **hashbrown**: apply ``response_format`` with the ``output_schema`` from context
The ``apply_render_mode`` function is a ``@wrap_model_call`` decorator for
LangGraph agents that plugs into the CopilotKit middleware chain.
"""
from __future__ import annotations
import json
from collections.abc import Mapping
from typing import Any
# ---------------------------------------------------------------------------
# Prompt fragments
# ---------------------------------------------------------------------------
JSONL_RENDER_INSTRUCTION = (
"\n\n## Output format — JSONL spec patches\n"
"You MUST emit your UI updates as JSONL (one JSON object per line) inside\n"
"a fenced code block with the ``spec`` language tag. Each line is a patch\n"
'object with at minimum an ``op`` field ("add", "replace", "remove")\n'
"and a ``path`` field (JSON-Pointer into the component tree).\n\n"
"Example:\n"
"```spec\n"
'{"op":"replace","path":"/title","value":"Updated Dashboard"}\n'
'{"op":"add","path":"/widgets/-","value":{"type":"chart","data":[1,2,3]}}\n'
"```\n"
"Do NOT wrap the block in any other markup. The frontend renderer will\n"
"parse each line and apply the patches incrementally.\n"
)
# ---------------------------------------------------------------------------
# Context extraction helpers
# ---------------------------------------------------------------------------
def get_render_mode(context: list[dict[str, Any]]) -> str:
"""Extract render_mode from CopilotKit context entries.
Scans the context list for an entry whose ``description`` is
``"render_mode"`` and returns its ``value``. Falls back to
``"tool-based"`` when no matching entry is found.
"""
for entry in context:
if entry.get("description") == "render_mode":
return entry.get("value", "tool-based")
return "tool-based"
def get_output_schema(context: list[dict[str, Any]]) -> dict[str, Any] | None:
"""Extract output_schema (HashBrown kit schema) from context.
Returns the parsed JSON schema dict, or ``None`` if the context does not
contain an ``output_schema`` entry.
"""
for entry in context:
if entry.get("description") == "output_schema":
val = entry.get("value")
if isinstance(val, str):
try:
return json.loads(val)
except json.JSONDecodeError:
return None
return val
return None
# ---------------------------------------------------------------------------
# Prompt augmentation
# ---------------------------------------------------------------------------
def apply_render_mode_prompt(system_prompt: str, render_mode: str) -> str:
"""Return *system_prompt* with render-mode instructions appended.
For ``tool-based`` and ``a2ui`` modes the prompt is returned unchanged.
For ``json-render`` the relevant instruction block is appended.
"""
if render_mode == "json-render":
return system_prompt + JSONL_RENDER_INSTRUCTION
return system_prompt
# ---------------------------------------------------------------------------
# LangGraph @wrap_model_call decorator
# ---------------------------------------------------------------------------
def apply_render_mode(fn=None):
"""``@wrap_model_call`` middleware that adapts the model request.
Usage with the CopilotKit middleware chain::
from middleware.render_mode import apply_render_mode
agent = create_agent(
...,
middleware=[CopilotKitMiddleware(), apply_render_mode()],
)
Behaviour per mode:
* **tool-based / a2ui** -- pass through unchanged.
* **json-render** -- prepend JSONL instruction to system messages.
* **hashbrown** -- set ``response_format`` with the ``output_schema``
extracted from context.
"""
try:
from langchain.agents.middleware import wrap_model_call
from langchain.agents.structured_output import ProviderStrategy
except ImportError:
# Fallback for environments without the CopilotKit langchain extensions
from copilotkit.langchain import wrap_model_call, ProviderStrategy
@wrap_model_call
async def _apply_render_mode(request, handler):
# --- Extract context from copilotkit state -------------------------
copilot_context: list[dict[str, Any]] | None = None
state = getattr(request, "state", None)
if isinstance(state, dict):
copilot_context = state.get("copilotkit", {}).get("context")
if not isinstance(copilot_context, list):
return await handler(request)
render_mode = get_render_mode(copilot_context)
# --- Prompt-injection modes ----------------------------------------
if render_mode == "json-render":
messages = list(getattr(request, "messages", []))
augmented = []
for msg in messages:
if getattr(msg, "type", None) == "system" or (
isinstance(msg, dict) and msg.get("role") == "system"
):
content = (
msg.content
if hasattr(msg, "content")
else msg.get("content", "")
)
new_content = apply_render_mode_prompt(content, render_mode)
if hasattr(msg, "content"):
# LangChain message object — copy with new content
msg = msg.copy(update={"content": new_content})
else:
msg = {**msg, "content": new_content}
augmented.append(msg)
request = request.override(messages=augmented)
# --- HashBrown mode: structured output via response_format ---------
elif render_mode == "hashbrown":
schema = get_output_schema(copilot_context)
if isinstance(schema, dict):
if not schema.get("title"):
schema["title"] = "StructuredOutput"
if not schema.get("description"):
schema["description"] = (
"Structured response schema for the CopilotKit agent."
)
request = request.override(
response_format=ProviderStrategy(schema=schema, strict=True),
)
return await handler(request)
if fn is not None:
return _apply_render_mode(fn)
return _apply_render_mode