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.
Files changed (38) hide show
  1. reflex_agentkit/__init__.py +68 -0
  2. reflex_agentkit/adapters/__init__.py +1 -0
  3. reflex_agentkit/adapters/_require.py +27 -0
  4. reflex_agentkit/adapters/langgraph.py +277 -0
  5. reflex_agentkit/adapters/pydantic_ai.py +93 -0
  6. reflex_agentkit/components/__init__.py +21 -0
  7. reflex_agentkit/components/approval.py +123 -0
  8. reflex_agentkit/components/chat.py +42 -0
  9. reflex_agentkit/components/composer.py +44 -0
  10. reflex_agentkit/components/messages.py +104 -0
  11. reflex_agentkit/components/shared_state.py +32 -0
  12. reflex_agentkit/components/status.py +41 -0
  13. reflex_agentkit/components/tools.py +97 -0
  14. reflex_agentkit/core/__init__.py +56 -0
  15. reflex_agentkit/core/_log.py +47 -0
  16. reflex_agentkit/core/_patch.py +46 -0
  17. reflex_agentkit/core/clients/__init__.py +14 -0
  18. reflex_agentkit/core/clients/base.py +71 -0
  19. reflex_agentkit/core/clients/http.py +147 -0
  20. reflex_agentkit/core/clients/in_process.py +67 -0
  21. reflex_agentkit/core/clients/sse.py +75 -0
  22. reflex_agentkit/core/commands.py +589 -0
  23. reflex_agentkit/core/conversation.py +93 -0
  24. reflex_agentkit/core/errors.py +74 -0
  25. reflex_agentkit/core/models.py +243 -0
  26. reflex_agentkit/core/reducer.py +1078 -0
  27. reflex_agentkit/core/run_input.py +122 -0
  28. reflex_agentkit/core/tools.py +204 -0
  29. reflex_agentkit/py.typed +0 -0
  30. reflex_agentkit/state/__init__.py +5 -0
  31. reflex_agentkit/state/_driver.py +520 -0
  32. reflex_agentkit/state/_projection.py +120 -0
  33. reflex_agentkit/state/_registry.py +46 -0
  34. reflex_agentkit/state/agent_state.py +496 -0
  35. reflex_agentkit-0.1.0.dist-info/METADATA +457 -0
  36. reflex_agentkit-0.1.0.dist-info/RECORD +38 -0
  37. reflex_agentkit-0.1.0.dist-info/WHEEL +4 -0
  38. 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)
File without changes
@@ -0,0 +1,5 @@
1
+ """Reflex state engine (AgentState mixin)."""
2
+
3
+ from reflex_agentkit.state.agent_state import AgentState
4
+
5
+ __all__ = ["AgentState"]