fifty-agent-sdk 1.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 (41) hide show
  1. fifty_agent_sdk/__init__.py +191 -0
  2. fifty_agent_sdk/audit/__init__.py +45 -0
  3. fifty_agent_sdk/audit/console.py +69 -0
  4. fifty_agent_sdk/audit/protocol.py +112 -0
  5. fifty_agent_sdk/audit/sql.py +328 -0
  6. fifty_agent_sdk/errors.py +119 -0
  7. fifty_agent_sdk/llm/__init__.py +30 -0
  8. fifty_agent_sdk/llm/openai_compat.py +345 -0
  9. fifty_agent_sdk/llm/protocol.py +87 -0
  10. fifty_agent_sdk/llm/types.py +149 -0
  11. fifty_agent_sdk/loop.py +836 -0
  12. fifty_agent_sdk/mcp/__init__.py +39 -0
  13. fifty_agent_sdk/mcp/client.py +622 -0
  14. fifty_agent_sdk/mcp/transport.py +177 -0
  15. fifty_agent_sdk/observability/__init__.py +18 -0
  16. fifty_agent_sdk/observability/hooks.py +232 -0
  17. fifty_agent_sdk/parser/__init__.py +32 -0
  18. fifty_agent_sdk/parser/base.py +118 -0
  19. fifty_agent_sdk/parser/json_mode.py +219 -0
  20. fifty_agent_sdk/parser/native_tools.py +93 -0
  21. fifty_agent_sdk/parser/prose_mode.py +162 -0
  22. fifty_agent_sdk/prompts.py +186 -0
  23. fifty_agent_sdk/py.typed +0 -0
  24. fifty_agent_sdk/runner.py +797 -0
  25. fifty_agent_sdk/safety.py +134 -0
  26. fifty_agent_sdk/state/__init__.py +53 -0
  27. fifty_agent_sdk/state/memory.py +156 -0
  28. fifty_agent_sdk/state/protocol.py +112 -0
  29. fifty_agent_sdk/state/redis.py +371 -0
  30. fifty_agent_sdk/state/sql.py +688 -0
  31. fifty_agent_sdk/streaming.py +273 -0
  32. fifty_agent_sdk/tools/__init__.py +32 -0
  33. fifty_agent_sdk/tools/inproc_provider.py +232 -0
  34. fifty_agent_sdk/tools/mcp_provider.py +345 -0
  35. fifty_agent_sdk/tools/protocol.py +124 -0
  36. fifty_agent_sdk/tools/registry.py +165 -0
  37. fifty_agent_sdk-1.1.0.dist-info/METADATA +225 -0
  38. fifty_agent_sdk-1.1.0.dist-info/RECORD +41 -0
  39. fifty_agent_sdk-1.1.0.dist-info/WHEEL +5 -0
  40. fifty_agent_sdk-1.1.0.dist-info/licenses/LICENSE +21 -0
  41. fifty_agent_sdk-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,191 @@
1
+ """fifty-agent-sdk — Production-grade reusable agent loop SDK.
2
+
3
+ Foundation layer: typed errors, Pydantic v2 message types, the
4
+ :class:`LLMClient` protocol, an OpenAI-compatible adapter, and a system
5
+ prompt template builder. Higher layers (parser, tool registry, ReACT loop,
6
+ state stores) build on these contracts in subsequent briefs.
7
+
8
+ Optional extra surface
9
+ :class:`SqlStateStore` and :data:`sql_metadata`, plus
10
+ :class:`SqlAuditSink` and :data:`audit_metadata` (all the ``sql``
11
+ extra, ``pip install 'fifty-agent-sdk[sql]'``), and :class:`RedisStateStore`
12
+ (the ``redis`` extra, ``pip install 'fifty-agent-sdk[redis]'``) are
13
+ re-exported lazily. Importing :mod:`fifty_agent_sdk` itself pulls neither
14
+ SQLAlchemy nor redis-py. First access to one of these symbols triggers
15
+ its import and raises a clear :class:`ImportError` if the relevant
16
+ extra is missing.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ from fifty_agent_sdk.audit import AuditEvent, AuditSink, ConsoleAuditSink
24
+ from fifty_agent_sdk.errors import (
25
+ AgentSdkError,
26
+ LLMError,
27
+ MaxIterationsExceeded,
28
+ MCPError,
29
+ ParserError,
30
+ StateStoreError,
31
+ ToolNotFound,
32
+ ToolTimeout,
33
+ )
34
+ from fifty_agent_sdk.llm import (
35
+ ChatMessage,
36
+ ChatRequest,
37
+ ChatResponse,
38
+ FinishReason,
39
+ LLMClient,
40
+ OpenAICompatibleClient,
41
+ Role,
42
+ ToolCall,
43
+ Usage,
44
+ )
45
+ from fifty_agent_sdk.loop import AgentLoop
46
+ from fifty_agent_sdk.mcp import MCPClient, MCPClientConfig, MCPToolDef
47
+ from fifty_agent_sdk.observability import Hooks
48
+ from fifty_agent_sdk.parser import (
49
+ FinalAnswer,
50
+ JsonModeParser,
51
+ NativeToolsParser,
52
+ NativeToolsParserStub,
53
+ Parser,
54
+ ParseResult,
55
+ ProseModeParser,
56
+ ThoughtAction,
57
+ )
58
+ from fifty_agent_sdk.prompts import (
59
+ JSON_MODE_OUTPUT_FORMAT,
60
+ PROSE_MODE_OUTPUT_FORMAT,
61
+ PromptSections,
62
+ json_mode_template,
63
+ prose_mode_template,
64
+ render_system_prompt,
65
+ )
66
+ from fifty_agent_sdk.runner import AgentRunner
67
+ from fifty_agent_sdk.safety import SafetyConfig
68
+ from fifty_agent_sdk.state import MemoryStateStore, StateStore
69
+ from fifty_agent_sdk.streaming import (
70
+ ActionEvent,
71
+ AgentEvent,
72
+ ErrorEvent,
73
+ FinalEvent,
74
+ ObservationEvent,
75
+ ThoughtEvent,
76
+ TokenEvent,
77
+ ToolFailedEvent,
78
+ ToolProgressEvent,
79
+ ToolStartedEvent,
80
+ )
81
+ from fifty_agent_sdk.tools import (
82
+ InProcProvider,
83
+ MCPProvider,
84
+ RefreshSummary,
85
+ Registry,
86
+ Tool,
87
+ ToolResult,
88
+ ToolSchema,
89
+ tool,
90
+ )
91
+
92
+ if TYPE_CHECKING:
93
+ from fifty_agent_sdk.audit.sql import SqlAuditSink, audit_metadata
94
+ from fifty_agent_sdk.state.redis import RedisStateStore
95
+ from fifty_agent_sdk.state.sql import SqlStateStore, sql_metadata
96
+
97
+ __version__ = "1.0.0"
98
+
99
+ __all__ = [
100
+ "JSON_MODE_OUTPUT_FORMAT",
101
+ "PROSE_MODE_OUTPUT_FORMAT",
102
+ "ActionEvent",
103
+ "AgentEvent",
104
+ "AgentLoop",
105
+ "AgentRunner",
106
+ "AgentSdkError",
107
+ "AuditEvent",
108
+ "AuditSink",
109
+ "ChatMessage",
110
+ "ChatRequest",
111
+ "ChatResponse",
112
+ "ConsoleAuditSink",
113
+ "ErrorEvent",
114
+ "FinalAnswer",
115
+ "FinalEvent",
116
+ "FinishReason",
117
+ "Hooks",
118
+ "InProcProvider",
119
+ "JsonModeParser",
120
+ "LLMClient",
121
+ "LLMError",
122
+ "MCPClient",
123
+ "MCPClientConfig",
124
+ "MCPError",
125
+ "MCPProvider",
126
+ "MCPToolDef",
127
+ "MaxIterationsExceeded",
128
+ "MemoryStateStore",
129
+ "NativeToolsParser",
130
+ "NativeToolsParserStub",
131
+ "ObservationEvent",
132
+ "OpenAICompatibleClient",
133
+ "ParseResult",
134
+ "Parser",
135
+ "ParserError",
136
+ "PromptSections",
137
+ "ProseModeParser",
138
+ "RedisStateStore",
139
+ "RefreshSummary",
140
+ "Registry",
141
+ "Role",
142
+ "SafetyConfig",
143
+ "SqlAuditSink",
144
+ "SqlStateStore",
145
+ "StateStore",
146
+ "StateStoreError",
147
+ "ThoughtAction",
148
+ "ThoughtEvent",
149
+ "TokenEvent",
150
+ "Tool",
151
+ "ToolCall",
152
+ "ToolFailedEvent",
153
+ "ToolNotFound",
154
+ "ToolProgressEvent",
155
+ "ToolResult",
156
+ "ToolSchema",
157
+ "ToolStartedEvent",
158
+ "ToolTimeout",
159
+ "Usage",
160
+ "audit_metadata",
161
+ "json_mode_template",
162
+ "prose_mode_template",
163
+ "render_system_prompt",
164
+ "sql_metadata",
165
+ "tool",
166
+ ]
167
+
168
+
169
+ def __getattr__(name: str) -> Any:
170
+ """Lazily import optional-extra surface symbols on first access.
171
+
172
+ ``SqlStateStore`` / ``sql_metadata`` and ``SqlAuditSink`` /
173
+ ``audit_metadata`` require the optional ``sql`` extra;
174
+ ``RedisStateStore`` requires the ``redis`` extra. Routing access
175
+ through this hook keeps ``import fifty_agent_sdk`` free of SQLAlchemy and
176
+ redis-py, and surfaces a clear :class:`ImportError` only when the
177
+ symbols are actually used without the extra installed.
178
+ """
179
+ if name in {"SqlStateStore", "sql_metadata"}:
180
+ from fifty_agent_sdk.state import sql
181
+
182
+ return getattr(sql, name)
183
+ if name in {"SqlAuditSink", "audit_metadata"}:
184
+ from fifty_agent_sdk.audit import sql as audit_sql
185
+
186
+ return getattr(audit_sql, name)
187
+ if name == "RedisStateStore":
188
+ from fifty_agent_sdk.state import redis
189
+
190
+ return redis.RedisStateStore
191
+ raise AttributeError(f"module 'fifty_agent_sdk' has no attribute {name!r}")
@@ -0,0 +1,45 @@
1
+ """Audit subpackage: pluggable, tamper-evident action provenance.
2
+
3
+ Re-exports the :class:`AuditEvent` model, the :class:`AuditSink` protocol,
4
+ and the dependency-free :class:`ConsoleAuditSink`. The durable SQL backend
5
+ (:class:`SqlAuditSink`) with its Alembic :data:`audit_metadata` symbol is
6
+ re-exported lazily via a module-level ``__getattr__`` — it requires the
7
+ optional ``sql`` extra, so ``import fifty_agent_sdk.audit`` itself does NOT pull
8
+ SQLAlchemy.
9
+
10
+ Accessing :data:`SqlAuditSink` or :data:`audit_metadata` without
11
+ ``fifty-agent-sdk[sql]`` installed raises a clear :class:`ImportError` referencing
12
+ the extras line.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ from fifty_agent_sdk.audit.console import ConsoleAuditSink
20
+ from fifty_agent_sdk.audit.protocol import AuditEvent, AuditSink
21
+
22
+ if TYPE_CHECKING:
23
+ from fifty_agent_sdk.audit.sql import SqlAuditSink, audit_metadata
24
+
25
+ __all__ = [
26
+ "AuditEvent",
27
+ "AuditSink",
28
+ "ConsoleAuditSink",
29
+ "SqlAuditSink",
30
+ "audit_metadata",
31
+ ]
32
+
33
+
34
+ def __getattr__(name: str) -> Any:
35
+ """Lazily import optional-extra surface symbols on first access.
36
+
37
+ Keeps the package's eager import surface free of SQLAlchemy. When the
38
+ ``sql`` extra is not installed, importing the backing module (triggered
39
+ here) raises a documented :class:`ImportError`.
40
+ """
41
+ if name in {"SqlAuditSink", "audit_metadata"}:
42
+ from fifty_agent_sdk.audit import sql
43
+
44
+ return getattr(sql, name)
45
+ raise AttributeError(f"module 'fifty_agent_sdk.audit' has no attribute {name!r}")
@@ -0,0 +1,69 @@
1
+ """ConsoleAuditSink — structured-log audit sink for dev / single-tenant use.
2
+
3
+ :class:`ConsoleAuditSink` is the zero-dependency :class:`AuditSink`
4
+ implementation. It emits each :class:`AuditEvent` as one ``INFO`` structlog
5
+ record under the fixed logger name ``fifty_agent_sdk.audit``.
6
+
7
+ It is the right choice when audit logs are already centralized (a single
8
+ deployment shipping structlog output to a log aggregator) and a separate
9
+ audit database is not warranted. It is NOT tamper-evident on its own — the
10
+ durability and integrity guarantees are whatever the surrounding log
11
+ pipeline provides. For a durable, queryable trail use
12
+ :class:`fifty_agent_sdk.audit.sql.SqlAuditSink`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Final
18
+
19
+ import structlog
20
+
21
+ from fifty_agent_sdk.audit.protocol import AuditEvent
22
+
23
+ _log: Final = structlog.get_logger("fifty_agent_sdk.audit")
24
+ """Module-level structured logger.
25
+
26
+ Bound to the fixed name ``fifty_agent_sdk.audit`` (NOT ``__name__``) so every
27
+ audit record — from this sink and from :mod:`fifty_agent_sdk.audit.sql` — shares
28
+ one logger name a consumer can route or filter on.
29
+ """
30
+
31
+
32
+ class ConsoleAuditSink:
33
+ """An :class:`AuditSink` that writes each event to structlog at ``INFO``.
34
+
35
+ Satisfies :class:`fifty_agent_sdk.audit.protocol.AuditSink` structurally (no
36
+ explicit inheritance needed thanks to ``@runtime_checkable``).
37
+
38
+ No constructor arguments — the sink is stateless and safe to share
39
+ across concurrent runs (structlog loggers are themselves thread- and
40
+ task-safe).
41
+
42
+ Caution:
43
+ Not tamper-evident on its own. The integrity of the trail is only
44
+ as strong as the log pipeline the records are shipped through. Use
45
+ :class:`fifty_agent_sdk.audit.sql.SqlAuditSink` when a durable, queryable,
46
+ append-only record is required.
47
+ """
48
+
49
+ async def record(self, event: AuditEvent) -> None:
50
+ """Emit ``event`` as a single ``INFO`` structlog record.
51
+
52
+ The record's event name is ``audit.event``; the
53
+ :class:`AuditEvent` fields are spread as structured key/values
54
+ (``timestamp`` rendered as an ISO-8601 string).
55
+
56
+ Args:
57
+ event: The :class:`AuditEvent` to log.
58
+ """
59
+ _log.info(
60
+ "audit.event",
61
+ session_id=event.session_id,
62
+ user_id=event.user_id,
63
+ event_type=event.event_type,
64
+ timestamp=event.timestamp.isoformat(),
65
+ payload=event.payload,
66
+ )
67
+
68
+
69
+ __all__ = ["ConsoleAuditSink"]
@@ -0,0 +1,112 @@
1
+ """AuditSink protocol — pluggable, tamper-evident action provenance.
2
+
3
+ The :class:`AuditSink` protocol is the SDK's abstraction for capturing a
4
+ durable record of every agent action. :class:`fifty_agent_sdk.runner.AgentRunner`
5
+ emits an :class:`AuditEvent` at four points of every ``run()``: session
6
+ start, each tool invocation, the final answer, and any error.
7
+
8
+ Implementations may write to structured logs (see
9
+ :class:`fifty_agent_sdk.audit.console.ConsoleAuditSink` — the dev default), a SQL
10
+ table (:class:`fifty_agent_sdk.audit.sql.SqlAuditSink`), or any other backend.
11
+
12
+ Contract invariants
13
+ * **Ordering.** :meth:`AuditSink.record` is called in the order events
14
+ occur within a run. Implementations MUST NOT reorder events; the
15
+ append-only nature of the record is a load-bearing property for any
16
+ consumer treating it as a compliance trail.
17
+ * **Concurrency.** :meth:`AuditSink.record` MUST be safe to call from
18
+ multiple concurrent runs sharing one sink instance. Implementations
19
+ that serialize do so internally; the caller does not lock.
20
+ * **Failure is the caller's to swallow.** A raising :meth:`record` is
21
+ tolerated by :class:`AgentRunner`, which catches the exception, logs a
22
+ ``WARNING``, and continues — an audit-sink outage MUST NOT abort a
23
+ live agent run. Implementations SHOULD still raise on backend failure
24
+ (rather than silently dropping) so the gap is visible in the Runner's
25
+ ``audit.emit_failed`` log line.
26
+ * **Tamper-evidence is the sink's job.** "Tamper-evident" is a property
27
+ of the *sink's storage* (append-only table, hash-chaining, WORM
28
+ bucket), not of the emission path. The emission path's contract is
29
+ best-effort delivery plus a loud warning when delivery fails.
30
+ * **No secrets in the payload.** Consumers and tools MUST NOT place
31
+ credentials or raw secrets into :attr:`AuditEvent.payload`; the
32
+ payload is persisted verbatim and may appear in logs.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from datetime import datetime
38
+ from typing import Any, Protocol, runtime_checkable
39
+
40
+ from pydantic import BaseModel, ConfigDict, Field
41
+
42
+
43
+ class AuditEvent(BaseModel):
44
+ """A single audited agent action.
45
+
46
+ The provider-agnostic shape passed to every :class:`AuditSink`. Like the
47
+ other SDK models it sets ``extra="forbid"`` so a typo'd field is caught
48
+ at construction rather than silently dropped.
49
+
50
+ Attributes:
51
+ session_id: Opaque session identifier the action belongs to.
52
+ user_id: Optional end-user identifier. The Runner does not have a
53
+ user-id channel today and always emits ``None``; consumers
54
+ wanting per-user attribution set it via a wrapping sink.
55
+ timestamp: When the action occurred (timezone-aware UTC). The Runner
56
+ stamps this at emission time — it is the *action* time, not the
57
+ time the row was persisted.
58
+ event_type: Short tag for the kind of action. The four values the
59
+ Runner emits are ``"session_start"``, ``"tool_invocation"``,
60
+ ``"final_answer"``, and ``"error"``; the field is a free-form
61
+ string (not a ``Literal``) so consumers can emit their own
62
+ event types through the same sink.
63
+ payload: Structured, event-specific detail. Always a real ``dict``
64
+ (never ``None``); defaults to ``{}``. MUST NOT contain
65
+ credentials or raw secrets — it is persisted and logged
66
+ verbatim.
67
+ """
68
+
69
+ model_config = ConfigDict(extra="forbid")
70
+
71
+ session_id: str
72
+ user_id: str | None = None
73
+ timestamp: datetime
74
+ event_type: str
75
+ payload: dict[str, Any] = Field(default_factory=dict)
76
+
77
+
78
+ @runtime_checkable
79
+ class AuditSink(Protocol):
80
+ """Pluggable audit-trail backend.
81
+
82
+ See the module docstring for the full contract. Implementations are
83
+ duck-typed: any class providing the single async method below with a
84
+ matching signature satisfies :func:`isinstance` against
85
+ :class:`AuditSink` thanks to ``@runtime_checkable``.
86
+
87
+ Note:
88
+ ``@runtime_checkable`` :class:`Protocol` instances only check for
89
+ method *presence*, not signature compatibility. Mypy ``--strict``
90
+ catches signature mismatches at type-check time.
91
+ """
92
+
93
+ async def record(self, event: AuditEvent) -> None:
94
+ """Persist a single :class:`AuditEvent`.
95
+
96
+ Called once per audited action, in occurrence order. Implementations
97
+ MUST NOT reorder events and MUST be safe to call concurrently from
98
+ multiple runs sharing the sink.
99
+
100
+ Args:
101
+ event: The :class:`AuditEvent` to record.
102
+
103
+ Raises:
104
+ Exception: Implementations MAY raise on backend failure. The
105
+ :class:`fifty_agent_sdk.runner.AgentRunner` catches the exception,
106
+ logs a ``WARNING``, and continues — a sink failure never
107
+ aborts an agent run.
108
+ """
109
+ ...
110
+
111
+
112
+ __all__ = ["AuditEvent", "AuditSink"]