reflex-agentkit 0.1.0rc1__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 +1065 -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.0rc1.dist-info/METADATA +457 -0
- reflex_agentkit-0.1.0rc1.dist-info/RECORD +38 -0
- reflex_agentkit-0.1.0rc1.dist-info/WHEEL +4 -0
- reflex_agentkit-0.1.0rc1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""``composer``: the message input (API §10, REQ-023.3, REQ-023.9)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import reflex as rx
|
|
8
|
+
|
|
9
|
+
__all__ = ["composer"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def composer(
|
|
13
|
+
state: type[Any], *, placeholder: str = "Send a message…", **props: Any
|
|
14
|
+
) -> rx.Component:
|
|
15
|
+
"""A text area and a send button; disabled unless ``state.can_send``.
|
|
16
|
+
|
|
17
|
+
Enter submits and Shift+Enter inserts a newline (REQ-023.3).
|
|
18
|
+
"""
|
|
19
|
+
props.setdefault("width", "100%")
|
|
20
|
+
return rx.form(
|
|
21
|
+
rx.hstack(
|
|
22
|
+
rx.text_area(
|
|
23
|
+
name="message",
|
|
24
|
+
placeholder=placeholder,
|
|
25
|
+
enter_key_submit=True,
|
|
26
|
+
disabled=~state.can_send,
|
|
27
|
+
aria_label="Message",
|
|
28
|
+
width="100%",
|
|
29
|
+
rows="2",
|
|
30
|
+
resize="vertical",
|
|
31
|
+
),
|
|
32
|
+
rx.button(
|
|
33
|
+
rx.icon("send", size=16, aria_hidden="true"),
|
|
34
|
+
type="submit",
|
|
35
|
+
disabled=~state.can_send,
|
|
36
|
+
aria_label="Send message",
|
|
37
|
+
),
|
|
38
|
+
align="end",
|
|
39
|
+
width="100%",
|
|
40
|
+
),
|
|
41
|
+
on_submit=lambda form: state.send_message(form.to(dict)["message"]),
|
|
42
|
+
reset_on_submit=True,
|
|
43
|
+
**props,
|
|
44
|
+
)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""``message_list``: the conversation in order (API §10, REQ-023.2, DD-015)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import reflex as rx
|
|
9
|
+
|
|
10
|
+
from reflex_agentkit.components.tools import ToolRenderer, tool_call_card
|
|
11
|
+
from reflex_agentkit.core.models import ChatMessage, ToolCall
|
|
12
|
+
|
|
13
|
+
__all__ = ["message_list"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _markdown(content: rx.Var[str]) -> rx.Component:
|
|
17
|
+
# Raw HTML is on by default in Reflex 0.10.1; model output must stay text (DD-015).
|
|
18
|
+
return rx.markdown(content, use_raw=False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _streaming_indicator(message: rx.Var[ChatMessage]) -> rx.Component:
|
|
22
|
+
return rx.cond(
|
|
23
|
+
message.streaming,
|
|
24
|
+
rx.spinner(size="1", aria_label="Streaming"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _tool_cards(
|
|
29
|
+
state: type[Any], message: rx.Var[ChatMessage], renderers: Mapping[str, ToolRenderer]
|
|
30
|
+
) -> rx.Component:
|
|
31
|
+
def card(tool_call: rx.Var[ToolCall]) -> rx.Component:
|
|
32
|
+
return rx.cond(
|
|
33
|
+
tool_call.parent_message_id == message.id,
|
|
34
|
+
tool_call_card(state, tool_call, renderers=renderers),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return rx.foreach(state.tool_calls, card)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _message(
|
|
41
|
+
state: type[Any], message: rx.Var[ChatMessage], renderers: Mapping[str, ToolRenderer]
|
|
42
|
+
) -> rx.Component:
|
|
43
|
+
return rx.match(
|
|
44
|
+
message.role,
|
|
45
|
+
(
|
|
46
|
+
"user",
|
|
47
|
+
rx.box(
|
|
48
|
+
rx.text(message.content, white_space="pre-wrap"),
|
|
49
|
+
align_self="flex-end",
|
|
50
|
+
background_color=rx.color("accent", 3),
|
|
51
|
+
padding="0.5em 0.75em",
|
|
52
|
+
border_radius="0.75em",
|
|
53
|
+
max_width="80%",
|
|
54
|
+
aria_label="User message",
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
(
|
|
58
|
+
"assistant",
|
|
59
|
+
rx.vstack(
|
|
60
|
+
rx.cond(
|
|
61
|
+
message.reasoning,
|
|
62
|
+
rx.el.details(
|
|
63
|
+
rx.el.summary("Reasoning"),
|
|
64
|
+
rx.text(message.reasoning, size="1", color_scheme="gray"),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
rx.cond(message.content, _markdown(message.content)),
|
|
68
|
+
_streaming_indicator(message),
|
|
69
|
+
_tool_cards(state, message, renderers),
|
|
70
|
+
align_self="flex-start",
|
|
71
|
+
width="100%",
|
|
72
|
+
spacing="2",
|
|
73
|
+
aria_label="Assistant message",
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"tool",
|
|
78
|
+
rx.el.details(
|
|
79
|
+
rx.el.summary("Tool result"),
|
|
80
|
+
rx.el.pre(message.content, style={"white_space": "pre-wrap"}),
|
|
81
|
+
aria_label="Tool message",
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
rx.fragment(),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def message_list(
|
|
89
|
+
state: type[Any], *, tool_renderers: Mapping[str, ToolRenderer] | None = None, **props: Any
|
|
90
|
+
) -> rx.Component:
|
|
91
|
+
"""User, assistant and tool messages in order, with tool calls under their message.
|
|
92
|
+
|
|
93
|
+
Assistant text is rendered as markdown without raw HTML (DD-015); tool arguments and
|
|
94
|
+
results are plain text.
|
|
95
|
+
"""
|
|
96
|
+
renderers = dict(tool_renderers or {})
|
|
97
|
+
props.setdefault("width", "100%")
|
|
98
|
+
props.setdefault("spacing", "3")
|
|
99
|
+
return rx.vstack(
|
|
100
|
+
rx.foreach(state.messages, lambda m: _message(state, m, renderers)),
|
|
101
|
+
role="log",
|
|
102
|
+
aria_live="polite",
|
|
103
|
+
**props,
|
|
104
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""``shared_state_panel`` (API §10, REQ-023.7)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import reflex as rx
|
|
8
|
+
|
|
9
|
+
__all__ = ["shared_state_panel"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def shared_state_panel(state: type[Any], **props: Any) -> rx.Component:
|
|
13
|
+
"""The shared state as JSON, with a marker while it is stale."""
|
|
14
|
+
props.setdefault("width", "100%")
|
|
15
|
+
return rx.card(
|
|
16
|
+
rx.vstack(
|
|
17
|
+
rx.hstack(
|
|
18
|
+
rx.text("Shared state", weight="medium", size="2"),
|
|
19
|
+
rx.cond(
|
|
20
|
+
state.shared_state_stale,
|
|
21
|
+
rx.badge("stale", color_scheme="amber", aria_label="Shared state is stale"),
|
|
22
|
+
),
|
|
23
|
+
align="center",
|
|
24
|
+
),
|
|
25
|
+
rx.el.pre(
|
|
26
|
+
state.shared_state.to_string(),
|
|
27
|
+
style={"white_space": "pre-wrap", "word_break": "break-word"},
|
|
28
|
+
),
|
|
29
|
+
spacing="2",
|
|
30
|
+
),
|
|
31
|
+
**props,
|
|
32
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""``run_status_bar`` (API §10, REQ-023.6)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import reflex as rx
|
|
8
|
+
|
|
9
|
+
__all__ = ["run_status_bar"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_status_bar(state: type[Any], **props: Any) -> rx.Component:
|
|
13
|
+
"""The run status and step, a cancel button while running, and the error."""
|
|
14
|
+
props.setdefault("width", "100%")
|
|
15
|
+
return rx.vstack(
|
|
16
|
+
rx.hstack(
|
|
17
|
+
rx.badge(state.run_status, variant="soft", aria_label="Run status"),
|
|
18
|
+
rx.cond(state.is_running, rx.spinner(size="1", aria_label="Running")),
|
|
19
|
+
rx.cond(state.current_step != "", rx.text(state.current_step, size="1")),
|
|
20
|
+
rx.spacer(),
|
|
21
|
+
rx.cond(
|
|
22
|
+
state.is_running,
|
|
23
|
+
rx.button(
|
|
24
|
+
"Cancel",
|
|
25
|
+
on_click=state.cancel_run,
|
|
26
|
+
variant="soft",
|
|
27
|
+
color_scheme="red",
|
|
28
|
+
size="1",
|
|
29
|
+
aria_label="Cancel run",
|
|
30
|
+
),
|
|
31
|
+
),
|
|
32
|
+
align="center",
|
|
33
|
+
width="100%",
|
|
34
|
+
),
|
|
35
|
+
rx.cond(
|
|
36
|
+
state.error != "",
|
|
37
|
+
rx.callout(state.error, icon="triangle_alert", color_scheme="red", role="alert"),
|
|
38
|
+
),
|
|
39
|
+
spacing="2",
|
|
40
|
+
**props,
|
|
41
|
+
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""``tool_call_card``: one tool call with its status (API §10, REQ-019, REQ-023.4, DD-013)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import reflex as rx
|
|
9
|
+
|
|
10
|
+
from reflex_agentkit.core.models import ToolCall
|
|
11
|
+
|
|
12
|
+
__all__ = ["ToolRenderer", "tool_call_card"]
|
|
13
|
+
|
|
14
|
+
ToolRenderer = Callable[[rx.Var[ToolCall]], rx.Component]
|
|
15
|
+
|
|
16
|
+
_BADGE_COLORS: dict[str, str] = {
|
|
17
|
+
"streaming": "gray",
|
|
18
|
+
"pending_approval": "amber",
|
|
19
|
+
"running": "blue",
|
|
20
|
+
"done": "green",
|
|
21
|
+
"rejected": "orange",
|
|
22
|
+
"error": "red",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _section(title: str, content: rx.Var[Any]) -> rx.Component:
|
|
27
|
+
"""A collapsible section whose content is shown as plain text, never markup (DD-015)."""
|
|
28
|
+
return rx.el.details(
|
|
29
|
+
rx.el.summary(title),
|
|
30
|
+
rx.el.pre(content, style={"white_space": "pre-wrap", "word_break": "break-word"}),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_tool_body(tool_call: rx.Var[ToolCall]) -> rx.Component:
|
|
35
|
+
"""Arguments and result as collapsible text sections (REQ-023.4)."""
|
|
36
|
+
return rx.vstack(
|
|
37
|
+
_section("Arguments", tool_call.args_text),
|
|
38
|
+
rx.cond(tool_call.result, _section("Result", tool_call.result)),
|
|
39
|
+
spacing="1",
|
|
40
|
+
width="100%",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _status_badge(tool_call: rx.Var[ToolCall]) -> rx.Component:
|
|
45
|
+
color = rx.match(
|
|
46
|
+
tool_call.status,
|
|
47
|
+
*((status, color) for status, color in _BADGE_COLORS.items()),
|
|
48
|
+
"gray",
|
|
49
|
+
)
|
|
50
|
+
return rx.badge(tool_call.status, color_scheme=color, variant="soft")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def tool_call_card(
|
|
54
|
+
state: type[Any],
|
|
55
|
+
tool_call: rx.Var[ToolCall],
|
|
56
|
+
*,
|
|
57
|
+
body: ToolRenderer | None = None,
|
|
58
|
+
renderers: Mapping[str, ToolRenderer] | None = None,
|
|
59
|
+
**props: Any,
|
|
60
|
+
) -> rx.Component:
|
|
61
|
+
"""A card with the tool name, a status badge and a body (API §10).
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
state: The ``AgentState`` subclass (kept for a uniform signature).
|
|
65
|
+
tool_call: The tool call var, usually from ``rx.foreach``.
|
|
66
|
+
body: Replaces the default body for every call rendered by this card.
|
|
67
|
+
renderers: Body renderers by tool name, resolved at compile time with
|
|
68
|
+
``rx.match`` (DD-013); other names use the default body.
|
|
69
|
+
props: Standard Reflex props for the root element.
|
|
70
|
+
"""
|
|
71
|
+
del state
|
|
72
|
+
if body is not None:
|
|
73
|
+
content = body(tool_call)
|
|
74
|
+
elif renderers:
|
|
75
|
+
content = rx.match(
|
|
76
|
+
tool_call.name,
|
|
77
|
+
*((name, render(tool_call)) for name, render in renderers.items()),
|
|
78
|
+
default_tool_body(tool_call),
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
content = default_tool_body(tool_call)
|
|
82
|
+
props.setdefault("width", "100%")
|
|
83
|
+
return rx.card(
|
|
84
|
+
rx.vstack(
|
|
85
|
+
rx.hstack(
|
|
86
|
+
rx.icon("wrench", size=14, aria_hidden="true"),
|
|
87
|
+
rx.text(tool_call.name, weight="medium", size="2"),
|
|
88
|
+
_status_badge(tool_call),
|
|
89
|
+
align="center",
|
|
90
|
+
spacing="2",
|
|
91
|
+
),
|
|
92
|
+
content,
|
|
93
|
+
spacing="2",
|
|
94
|
+
width="100%",
|
|
95
|
+
),
|
|
96
|
+
**props,
|
|
97
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Pure core: AG-UI models, reducer and clients. Must never import reflex or any extra."""
|
|
2
|
+
|
|
3
|
+
from reflex_agentkit.core.clients import (
|
|
4
|
+
AgentClient,
|
|
5
|
+
AgentSource,
|
|
6
|
+
AGUIAgent,
|
|
7
|
+
HttpAGUIClient,
|
|
8
|
+
InProcessAGUIClient,
|
|
9
|
+
)
|
|
10
|
+
from reflex_agentkit.core.conversation import ConversationState
|
|
11
|
+
from reflex_agentkit.core.errors import (
|
|
12
|
+
AdapterNotInstalledError,
|
|
13
|
+
AgentHTTPError,
|
|
14
|
+
AgentKitError,
|
|
15
|
+
AgentStreamError,
|
|
16
|
+
ConfigurationError,
|
|
17
|
+
ErrorCode,
|
|
18
|
+
)
|
|
19
|
+
from reflex_agentkit.core.models import (
|
|
20
|
+
ChatMessage,
|
|
21
|
+
CustomEventRecord,
|
|
22
|
+
Diagnostic,
|
|
23
|
+
PendingInterrupt,
|
|
24
|
+
RunStatus,
|
|
25
|
+
ToolCall,
|
|
26
|
+
ToolCallStatus,
|
|
27
|
+
)
|
|
28
|
+
from reflex_agentkit.core.reducer import reduce
|
|
29
|
+
from reflex_agentkit.core.run_input import RunInputBuilder
|
|
30
|
+
from reflex_agentkit.core.tools import ClientTool, client_tool
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AGUIAgent",
|
|
34
|
+
"AdapterNotInstalledError",
|
|
35
|
+
"AgentClient",
|
|
36
|
+
"AgentHTTPError",
|
|
37
|
+
"AgentKitError",
|
|
38
|
+
"AgentSource",
|
|
39
|
+
"AgentStreamError",
|
|
40
|
+
"ChatMessage",
|
|
41
|
+
"ClientTool",
|
|
42
|
+
"ConfigurationError",
|
|
43
|
+
"ConversationState",
|
|
44
|
+
"CustomEventRecord",
|
|
45
|
+
"Diagnostic",
|
|
46
|
+
"ErrorCode",
|
|
47
|
+
"HttpAGUIClient",
|
|
48
|
+
"InProcessAGUIClient",
|
|
49
|
+
"PendingInterrupt",
|
|
50
|
+
"RunInputBuilder",
|
|
51
|
+
"RunStatus",
|
|
52
|
+
"ToolCall",
|
|
53
|
+
"ToolCallStatus",
|
|
54
|
+
"client_tool",
|
|
55
|
+
"reduce",
|
|
56
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Library logging (TD §7.1, REQ-027).
|
|
2
|
+
|
|
3
|
+
Every logger lives under ``reflex_agentkit``. Run-scoped records go through
|
|
4
|
+
:class:`RunLogger`, which puts ``thread_id`` and ``run_id`` both in the message prefix
|
|
5
|
+
and as record attributes (REQ-027.1). Message contents, tool args and results are only
|
|
6
|
+
ever logged at DEBUG (REQ-027.2).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from collections.abc import MutableMapping
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
__all__ = ["LOGGER_NAME", "RunLogger", "get_logger"]
|
|
16
|
+
|
|
17
|
+
LOGGER_NAME = "reflex_agentkit"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_logger(name: str) -> logging.Logger:
|
|
21
|
+
"""The child logger ``reflex_agentkit.<name>``."""
|
|
22
|
+
return logging.getLogger(f"{LOGGER_NAME}.{name}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RunLogger(logging.LoggerAdapter[logging.Logger]):
|
|
26
|
+
"""Adds the run context to every record."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, logger: logging.Logger, *, thread_id: str, run_id: str) -> None:
|
|
29
|
+
super().__init__(logger, {"thread_id": thread_id, "run_id": run_id})
|
|
30
|
+
|
|
31
|
+
def with_run(self, run_id: str) -> RunLogger:
|
|
32
|
+
"""The same logger for a follow-up run of the thread."""
|
|
33
|
+
return RunLogger(self.logger, thread_id=self.thread_id, run_id=run_id)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def thread_id(self) -> str:
|
|
37
|
+
return str((self.extra or {})["thread_id"])
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def run_id(self) -> str:
|
|
41
|
+
return str((self.extra or {})["run_id"])
|
|
42
|
+
|
|
43
|
+
def process(
|
|
44
|
+
self, msg: Any, kwargs: MutableMapping[str, Any]
|
|
45
|
+
) -> tuple[Any, MutableMapping[str, Any]]:
|
|
46
|
+
kwargs["extra"] = {**(self.extra or {}), **kwargs.get("extra", {})}
|
|
47
|
+
return f"[thread={self.thread_id} run={self.run_id}] {msg}", kwargs
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Typed façade over ``jsonpatch`` for RFC 6902 shared-state deltas (TD DD-007).
|
|
2
|
+
|
|
3
|
+
This is the only module that imports ``jsonpatch``. Patches are applied to a deep copy,
|
|
4
|
+
so a failure anywhere leaves the caller's document untouched (REQ-016.3).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
import jsonpatch # type: ignore[import-untyped] # jsonpatch ships no stubs (DD-007, Art. 7)
|
|
12
|
+
|
|
13
|
+
from reflex_agentkit.core.models import JsonObject
|
|
14
|
+
|
|
15
|
+
__all__ = ["PatchError", "apply_patch"]
|
|
16
|
+
|
|
17
|
+
_PATCH_FAILURES: tuple[type[BaseException], ...] = (
|
|
18
|
+
jsonpatch.JsonPatchException,
|
|
19
|
+
jsonpatch.JsonPointerException,
|
|
20
|
+
AttributeError,
|
|
21
|
+
IndexError,
|
|
22
|
+
KeyError,
|
|
23
|
+
RecursionError, # pathologically deep documents or values
|
|
24
|
+
TypeError,
|
|
25
|
+
ValueError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PatchError(Exception):
|
|
30
|
+
"""An RFC 6902 patch could not be applied; the input is unchanged."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def apply_patch(doc: JsonObject, ops: list[JsonObject]) -> JsonObject:
|
|
34
|
+
"""Apply ``ops`` atomically to a deep copy of ``doc`` and return the result.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
PatchError: on any failure (bad path, failed ``test`` op, malformed operation,
|
|
38
|
+
or a result that is not a JSON object).
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
result: Any = jsonpatch.apply_patch(doc, ops, in_place=False)
|
|
42
|
+
except _PATCH_FAILURES as exc:
|
|
43
|
+
raise PatchError(f"JSON Patch failed: {exc}") from exc
|
|
44
|
+
if not isinstance(result, dict):
|
|
45
|
+
raise PatchError("JSON Patch failed: the result is not a JSON object")
|
|
46
|
+
return cast(JsonObject, result)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""AG-UI clients (HTTP/SSE and in-process) and the protocols they implement."""
|
|
2
|
+
|
|
3
|
+
from reflex_agentkit.core.clients.base import AgentClient, AgentSource, AGUIAgent, validate_event
|
|
4
|
+
from reflex_agentkit.core.clients.http import HttpAGUIClient
|
|
5
|
+
from reflex_agentkit.core.clients.in_process import InProcessAGUIClient
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AGUIAgent",
|
|
9
|
+
"AgentClient",
|
|
10
|
+
"AgentSource",
|
|
11
|
+
"HttpAGUIClient",
|
|
12
|
+
"InProcessAGUIClient",
|
|
13
|
+
"validate_event",
|
|
14
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Client protocols and validated event decoding (API §7.1-7.2).
|
|
2
|
+
|
|
3
|
+
``AGUIAgent`` is what adapters implement (they yield AG-UI events for a run input);
|
|
4
|
+
``AgentClient`` is what ``AgentState`` consumes. Both are runtime-checkable, so
|
|
5
|
+
``AgentState.agent`` can hold either (``AgentSource``). Clients validate every event
|
|
6
|
+
with :func:`validate_event`, which skips and logs anything invalid (REQ-012.2-3).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from collections.abc import AsyncIterator
|
|
13
|
+
from typing import Protocol, TypeAlias, runtime_checkable
|
|
14
|
+
|
|
15
|
+
from ag_ui.core import BaseEvent, RunAgentInput
|
|
16
|
+
|
|
17
|
+
from reflex_agentkit.core.models import Diagnostic
|
|
18
|
+
from reflex_agentkit.core.reducer import parse_event
|
|
19
|
+
|
|
20
|
+
__all__ = ["AGUIAgent", "AgentClient", "AgentSource", "validate_event"]
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("reflex_agentkit.core.clients")
|
|
23
|
+
|
|
24
|
+
_LEVELS = {"debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARNING}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@runtime_checkable
|
|
28
|
+
class AGUIAgent(Protocol):
|
|
29
|
+
"""An in-process agent that yields AG-UI events for a run (API §7.1)."""
|
|
30
|
+
|
|
31
|
+
def run(self, run_input: RunAgentInput) -> AsyncIterator[BaseEvent]:
|
|
32
|
+
"""Yield the run's events, from ``RUN_STARTED`` to one terminal event."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@runtime_checkable
|
|
37
|
+
class AgentClient(Protocol):
|
|
38
|
+
"""The transport ``AgentState`` streams from (API §7.2)."""
|
|
39
|
+
|
|
40
|
+
def stream(self, run_input: RunAgentInput) -> AsyncIterator[BaseEvent]:
|
|
41
|
+
"""Yield validated events; raise ``AgentHTTPError``/``AgentStreamError`` on failure."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
async def aclose(self) -> None:
|
|
45
|
+
"""Release pooled resources. Idempotent."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
AgentSource: TypeAlias = AgentClient | AGUIAgent
|
|
50
|
+
"""What ``AgentState.agent`` accepts; an ``AGUIAgent`` is wrapped in-process."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_event(raw: object) -> BaseEvent | None:
|
|
54
|
+
"""Validate a wire dict or an event object; ``None`` (logged) if it must be skipped.
|
|
55
|
+
|
|
56
|
+
Event objects are re-validated through their camelCase dump, so events built with
|
|
57
|
+
``model_construct`` cannot bypass the schema (REQ-012.3).
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(raw, BaseEvent):
|
|
60
|
+
try:
|
|
61
|
+
raw = raw.model_dump(mode="json", by_alias=True, exclude_none=True, warnings=False)
|
|
62
|
+
except (TypeError, ValueError) as exc: # pragma: no cover - defensive
|
|
63
|
+
logger.warning(
|
|
64
|
+
"skipped an AG-UI event that cannot be serialized: %s", type(exc).__name__
|
|
65
|
+
)
|
|
66
|
+
return None
|
|
67
|
+
result = parse_event(raw)
|
|
68
|
+
if isinstance(result, Diagnostic):
|
|
69
|
+
logger.log(_LEVELS.get(result.level, logging.WARNING), "%s", result.message)
|
|
70
|
+
return None
|
|
71
|
+
return result
|