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.
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 +1065 -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.0rc1.dist-info/METADATA +457 -0
  36. reflex_agentkit-0.1.0rc1.dist-info/RECORD +38 -0
  37. reflex_agentkit-0.1.0rc1.dist-info/WHEEL +4 -0
  38. 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