reflex-agentkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reflex_agentkit/__init__.py +68 -0
- reflex_agentkit/adapters/__init__.py +1 -0
- reflex_agentkit/adapters/_require.py +27 -0
- reflex_agentkit/adapters/langgraph.py +277 -0
- reflex_agentkit/adapters/pydantic_ai.py +93 -0
- reflex_agentkit/components/__init__.py +21 -0
- reflex_agentkit/components/approval.py +123 -0
- reflex_agentkit/components/chat.py +42 -0
- reflex_agentkit/components/composer.py +44 -0
- reflex_agentkit/components/messages.py +104 -0
- reflex_agentkit/components/shared_state.py +32 -0
- reflex_agentkit/components/status.py +41 -0
- reflex_agentkit/components/tools.py +97 -0
- reflex_agentkit/core/__init__.py +56 -0
- reflex_agentkit/core/_log.py +47 -0
- reflex_agentkit/core/_patch.py +46 -0
- reflex_agentkit/core/clients/__init__.py +14 -0
- reflex_agentkit/core/clients/base.py +71 -0
- reflex_agentkit/core/clients/http.py +147 -0
- reflex_agentkit/core/clients/in_process.py +67 -0
- reflex_agentkit/core/clients/sse.py +75 -0
- reflex_agentkit/core/commands.py +589 -0
- reflex_agentkit/core/conversation.py +93 -0
- reflex_agentkit/core/errors.py +74 -0
- reflex_agentkit/core/models.py +243 -0
- reflex_agentkit/core/reducer.py +1078 -0
- reflex_agentkit/core/run_input.py +122 -0
- reflex_agentkit/core/tools.py +204 -0
- reflex_agentkit/py.typed +0 -0
- reflex_agentkit/state/__init__.py +5 -0
- reflex_agentkit/state/_driver.py +520 -0
- reflex_agentkit/state/_projection.py +120 -0
- reflex_agentkit/state/_registry.py +46 -0
- reflex_agentkit/state/agent_state.py +496 -0
- reflex_agentkit-0.1.0.dist-info/METADATA +457 -0
- reflex_agentkit-0.1.0.dist-info/RECORD +38 -0
- reflex_agentkit-0.1.0.dist-info/WHEEL +4 -0
- reflex_agentkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""``RunInputBuilder``: ``ConversationState`` -> ``RunAgentInput`` (DM §7.3, API §8.1).
|
|
2
|
+
|
|
3
|
+
The full history is sent on every run. Reasoning records go back immediately before
|
|
4
|
+
their assistant message with their encrypted value (REQ-011.2); unlinked records are
|
|
5
|
+
appended at the end. Assistant messages carry their tool calls with the current (possibly
|
|
6
|
+
edited) arguments. ``resume`` is present only when the run answers interrupts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
import json
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
15
|
+
from ag_ui.core import (
|
|
16
|
+
AssistantMessage,
|
|
17
|
+
DeveloperMessage,
|
|
18
|
+
FunctionCall,
|
|
19
|
+
Message,
|
|
20
|
+
ReasoningMessage,
|
|
21
|
+
RunAgentInput,
|
|
22
|
+
SystemMessage,
|
|
23
|
+
ToolMessage,
|
|
24
|
+
UserMessage,
|
|
25
|
+
)
|
|
26
|
+
from ag_ui.core import ToolCall as AguiToolCall
|
|
27
|
+
|
|
28
|
+
from reflex_agentkit.core.commands import resume_entries
|
|
29
|
+
from reflex_agentkit.core.conversation import ConversationState
|
|
30
|
+
from reflex_agentkit.core.models import ChatMessage, MessageRole, ReasoningRecord
|
|
31
|
+
from reflex_agentkit.core.tools import ClientTool
|
|
32
|
+
|
|
33
|
+
__all__ = ["PROTOCOL_VERSION", "RunInputBuilder", "to_agui_messages"]
|
|
34
|
+
|
|
35
|
+
PROTOCOL_VERSION = "1.0"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _reasoning(record: ReasoningRecord) -> ReasoningMessage | None:
|
|
39
|
+
if not record.content and record.encrypted_value is None:
|
|
40
|
+
return None
|
|
41
|
+
return ReasoningMessage(
|
|
42
|
+
id=record.id, content=record.content, encrypted_value=record.encrypted_value
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _assistant(conv: ConversationState, msg: ChatMessage) -> AssistantMessage:
|
|
47
|
+
calls = []
|
|
48
|
+
for call_id in msg.tool_call_ids:
|
|
49
|
+
index = conv._tool_index.get(call_id)
|
|
50
|
+
if index is None: # pragma: no cover - excluded by INV-3
|
|
51
|
+
continue
|
|
52
|
+
call = conv.tool_calls[index]
|
|
53
|
+
arguments = json.dumps(call.args or {})
|
|
54
|
+
calls.append(
|
|
55
|
+
AguiToolCall(id=call.id, function=FunctionCall(name=call.name, arguments=arguments))
|
|
56
|
+
)
|
|
57
|
+
return AssistantMessage(id=msg.id, content=msg.content or None, tool_calls=calls or None)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _message(conv: ConversationState, msg: ChatMessage) -> Message:
|
|
61
|
+
match msg.role:
|
|
62
|
+
case MessageRole.USER:
|
|
63
|
+
return UserMessage(id=msg.id, content=msg.content)
|
|
64
|
+
case MessageRole.SYSTEM:
|
|
65
|
+
return SystemMessage(id=msg.id, content=msg.content)
|
|
66
|
+
case MessageRole.DEVELOPER:
|
|
67
|
+
return DeveloperMessage(id=msg.id, content=msg.content)
|
|
68
|
+
case MessageRole.ASSISTANT:
|
|
69
|
+
return _assistant(conv, msg)
|
|
70
|
+
return ToolMessage(
|
|
71
|
+
id=msg.id,
|
|
72
|
+
content=msg.content,
|
|
73
|
+
tool_call_id=msg.tool_call_id or "",
|
|
74
|
+
error=msg.content if msg.is_error else None,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def to_agui_messages(conv: ConversationState) -> list[Message]:
|
|
79
|
+
"""The conversation history as AG-UI messages (DM §7.3)."""
|
|
80
|
+
by_assistant: dict[str, list[ReasoningRecord]] = {}
|
|
81
|
+
loose: list[ReasoningRecord] = []
|
|
82
|
+
message_ids = conv._message_index
|
|
83
|
+
for record in conv.reasoning:
|
|
84
|
+
if record.assistant_message_id in message_ids:
|
|
85
|
+
by_assistant.setdefault(record.assistant_message_id, []).append(record)
|
|
86
|
+
else:
|
|
87
|
+
loose.append(record)
|
|
88
|
+
|
|
89
|
+
out: list[Message] = []
|
|
90
|
+
for msg in conv.messages:
|
|
91
|
+
for record in by_assistant.get(msg.id, ()):
|
|
92
|
+
if (reasoning := _reasoning(record)) is not None:
|
|
93
|
+
out.append(reasoning)
|
|
94
|
+
out.append(_message(conv, msg))
|
|
95
|
+
out.extend(r for record in loose if (r := _reasoning(record)) is not None)
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
100
|
+
class RunInputBuilder:
|
|
101
|
+
"""Builds the ``RunAgentInput`` of a run (API §8.1).
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
client_tools: The tools advertised in ``tools`` (REQ-018.1).
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
client_tools: Sequence[ClientTool] = ()
|
|
108
|
+
|
|
109
|
+
def build(self, conv: ConversationState) -> RunAgentInput:
|
|
110
|
+
"""The input for the run that ``begin_run`` started on ``conv``."""
|
|
111
|
+
resume = resume_entries(conv)
|
|
112
|
+
return RunAgentInput(
|
|
113
|
+
thread_id=conv.thread_id,
|
|
114
|
+
run_id=conv.run_id,
|
|
115
|
+
protocol_version=PROTOCOL_VERSION,
|
|
116
|
+
state=conv.shared_state,
|
|
117
|
+
messages=to_agui_messages(conv),
|
|
118
|
+
tools=[tool.to_agui_tool() for tool in self.client_tools],
|
|
119
|
+
context=[],
|
|
120
|
+
forwarded_props={},
|
|
121
|
+
resume=resume or None,
|
|
122
|
+
)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Client-side tools: Python handlers the agent calls and the app executes (API §6).
|
|
2
|
+
|
|
3
|
+
``@client_tool`` turns a function into a frozen :class:`ClientTool` whose JSON Schema
|
|
4
|
+
``parameters`` come from the type hints (via a pydantic model). ``ClientTool.invoke``
|
|
5
|
+
validates the arguments, runs the handler (sync handlers in a worker thread, Constitution
|
|
6
|
+
Art. 4) under a timeout and returns ``(ok, text)``: handler failures are tool results,
|
|
7
|
+
never exceptions (REQ-018.4).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import dataclasses
|
|
14
|
+
import functools
|
|
15
|
+
import inspect
|
|
16
|
+
import typing
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from typing import Any, overload
|
|
19
|
+
|
|
20
|
+
import pydantic
|
|
21
|
+
import pydantic_core
|
|
22
|
+
from ag_ui.core import Tool
|
|
23
|
+
|
|
24
|
+
from reflex_agentkit.core.errors import ConfigurationError
|
|
25
|
+
|
|
26
|
+
__all__ = ["ClientTool", "client_tool"]
|
|
27
|
+
|
|
28
|
+
Handler = Callable[..., Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
32
|
+
class ClientTool:
|
|
33
|
+
"""A tool advertised to the agent and executed in the app (API §6)."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
description: str
|
|
37
|
+
parameters: dict[str, Any]
|
|
38
|
+
handler: Handler = dataclasses.field(repr=False)
|
|
39
|
+
requires_approval: bool = False
|
|
40
|
+
timeout_s: float | None = None
|
|
41
|
+
_model: type[pydantic.BaseModel] = dataclasses.field(init=False, repr=False, compare=False)
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
object.__setattr__(self, "_model", _model_for(self.handler, self.name))
|
|
45
|
+
|
|
46
|
+
def to_agui_tool(self) -> Tool:
|
|
47
|
+
"""The definition sent in ``RunAgentInput.tools`` (REQ-018.1)."""
|
|
48
|
+
return Tool(name=self.name, description=self.description, parameters=self.parameters)
|
|
49
|
+
|
|
50
|
+
async def invoke(self, args: dict[str, Any], timeout_s: float | None) -> tuple[bool, str]:
|
|
51
|
+
"""Run the handler with ``args``; return ``(ok, result text)`` (REQ-018.2, 018.4-5).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
args: The parsed (or edited) call arguments.
|
|
55
|
+
timeout_s: Seconds before the call is treated as failed; ``None`` for no limit.
|
|
56
|
+
A timed-out sync handler's thread cannot be interrupted and runs to
|
|
57
|
+
completion in the background; its result is discarded.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
validated = self._model.model_validate(args)
|
|
61
|
+
except pydantic.ValidationError as exc:
|
|
62
|
+
return False, f"Invalid tool arguments: {exc}"
|
|
63
|
+
kwargs = {name: getattr(validated, name) for name in type(validated).model_fields}
|
|
64
|
+
try:
|
|
65
|
+
async with asyncio.timeout(timeout_s):
|
|
66
|
+
result = await self._call(kwargs)
|
|
67
|
+
except TimeoutError:
|
|
68
|
+
return False, f"The tool timed out after {timeout_s} s."
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
return False, f"{type(exc).__name__}: {exc}"
|
|
71
|
+
return _encode(result)
|
|
72
|
+
|
|
73
|
+
async def _call(self, kwargs: dict[str, Any]) -> Any:
|
|
74
|
+
if inspect.iscoroutinefunction(self.handler):
|
|
75
|
+
return await self.handler(**kwargs)
|
|
76
|
+
result = await asyncio.to_thread(self.handler, **kwargs)
|
|
77
|
+
if inspect.isawaitable(result):
|
|
78
|
+
result = await result
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _encode(result: Any) -> tuple[bool, str]:
|
|
83
|
+
"""``str`` as-is, ``None`` as ``""``, anything else as compact JSON (API §6)."""
|
|
84
|
+
if result is None:
|
|
85
|
+
return True, ""
|
|
86
|
+
if isinstance(result, str):
|
|
87
|
+
return True, result
|
|
88
|
+
try:
|
|
89
|
+
return True, pydantic_core.to_json(result).decode()
|
|
90
|
+
except pydantic_core.PydanticSerializationError as exc:
|
|
91
|
+
return False, f"The tool result is not JSON-encodable: {exc}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _description(func: Handler) -> str:
|
|
95
|
+
doc = inspect.getdoc(func)
|
|
96
|
+
if not doc:
|
|
97
|
+
return ""
|
|
98
|
+
return " ".join(doc.split("\n\n", 1)[0].split())
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _model_for(func: Handler, name: str) -> type[pydantic.BaseModel]:
|
|
102
|
+
"""A pydantic model of ``func``'s parameters; ``ConfigurationError`` if unsupported."""
|
|
103
|
+
try:
|
|
104
|
+
signature = inspect.signature(func)
|
|
105
|
+
target = func.func if isinstance(func, functools.partial) else func
|
|
106
|
+
hints = typing.get_type_hints(target, include_extras=True)
|
|
107
|
+
except (TypeError, ValueError, NameError) as exc:
|
|
108
|
+
raise ConfigurationError(f"client tool {name!r}: cannot read its signature: {exc}") from exc
|
|
109
|
+
fields: dict[str, Any] = {}
|
|
110
|
+
for param in signature.parameters.values():
|
|
111
|
+
if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD, param.POSITIONAL_ONLY):
|
|
112
|
+
raise ConfigurationError(
|
|
113
|
+
f"client tool {name!r}: parameter {param.name!r} must be a named parameter"
|
|
114
|
+
)
|
|
115
|
+
if param.name not in hints:
|
|
116
|
+
raise ConfigurationError(
|
|
117
|
+
f"client tool {name!r}: parameter {param.name!r} has no type hint"
|
|
118
|
+
)
|
|
119
|
+
default = ... if param.default is param.empty else param.default
|
|
120
|
+
fields[param.name] = (hints[param.name], default)
|
|
121
|
+
try:
|
|
122
|
+
model: type[pydantic.BaseModel] = pydantic.create_model(f"{name}_args", **fields)
|
|
123
|
+
model.model_json_schema()
|
|
124
|
+
except (pydantic.PydanticUserError, NameError, TypeError) as exc:
|
|
125
|
+
raise ConfigurationError(
|
|
126
|
+
f"client tool {name!r}: its parameters cannot be described as JSON Schema: {exc}"
|
|
127
|
+
) from exc
|
|
128
|
+
return model
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _build(
|
|
132
|
+
func: Handler,
|
|
133
|
+
*,
|
|
134
|
+
name: str | None,
|
|
135
|
+
description: str | None,
|
|
136
|
+
requires_approval: bool,
|
|
137
|
+
timeout_s: float | None,
|
|
138
|
+
) -> ClientTool:
|
|
139
|
+
if not callable(func):
|
|
140
|
+
raise ConfigurationError(f"client_tool expects a function, got {type(func).__name__}")
|
|
141
|
+
tool_name = name or getattr(func, "__name__", "")
|
|
142
|
+
if not tool_name:
|
|
143
|
+
raise ConfigurationError("client_tool: the tool needs a name")
|
|
144
|
+
model = _model_for(func, tool_name)
|
|
145
|
+
schema = model.model_json_schema()
|
|
146
|
+
schema.pop("title", None)
|
|
147
|
+
schema.setdefault("properties", {})
|
|
148
|
+
return ClientTool(
|
|
149
|
+
name=tool_name,
|
|
150
|
+
description=_description(func) if description is None else description,
|
|
151
|
+
parameters=schema,
|
|
152
|
+
handler=func,
|
|
153
|
+
requires_approval=requires_approval,
|
|
154
|
+
timeout_s=timeout_s,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@overload
|
|
159
|
+
def client_tool(func: Handler, /) -> ClientTool: ...
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@overload
|
|
163
|
+
def client_tool(
|
|
164
|
+
*,
|
|
165
|
+
name: str | None = None,
|
|
166
|
+
description: str | None = None,
|
|
167
|
+
requires_approval: bool = False,
|
|
168
|
+
timeout_s: float | None = None,
|
|
169
|
+
) -> Callable[[Handler], ClientTool]: ...
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def client_tool(
|
|
173
|
+
func: Handler | None = None,
|
|
174
|
+
/,
|
|
175
|
+
*,
|
|
176
|
+
name: str | None = None,
|
|
177
|
+
description: str | None = None,
|
|
178
|
+
requires_approval: bool = False,
|
|
179
|
+
timeout_s: float | None = None,
|
|
180
|
+
) -> ClientTool | Callable[[Handler], ClientTool]:
|
|
181
|
+
"""Declare a client-side tool (API §6, REQ-018.1).
|
|
182
|
+
|
|
183
|
+
Usable bare (``@client_tool``) or with options (``@client_tool(requires_approval=True)``).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
func: The handler, ``async def`` or plain ``def``.
|
|
187
|
+
name: Tool name; defaults to the function name.
|
|
188
|
+
description: Defaults to the first paragraph of the docstring.
|
|
189
|
+
requires_approval: Whether each call waits for ``approve_tool`` (REQ-018.3).
|
|
190
|
+
timeout_s: Per-call timeout; ``None`` uses ``AgentState.client_tool_timeout_s``.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ConfigurationError: A parameter has no type hint or one that cannot be expressed
|
|
194
|
+
as JSON Schema, or the parameters use ``*args``/``**kwargs``/positional-only.
|
|
195
|
+
"""
|
|
196
|
+
options: dict[str, Any] = {
|
|
197
|
+
"name": name,
|
|
198
|
+
"description": description,
|
|
199
|
+
"requires_approval": requires_approval,
|
|
200
|
+
"timeout_s": timeout_s,
|
|
201
|
+
}
|
|
202
|
+
if func is None:
|
|
203
|
+
return lambda f: _build(f, **options)
|
|
204
|
+
return _build(func, **options)
|
reflex_agentkit/py.typed
ADDED
|
File without changes
|