sofias-sdk-lite 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/PKG-INFO +11 -6
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/README.md +10 -5
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/pyproject.toml +1 -1
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/pyproject.toml.orig +1 -1
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/agent.py +3 -1
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/agent_builder.py +6 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/agent_config.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/middleware.py +3 -1
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/response_workflow.py +7 -1
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/agent_settings.py +3 -2
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/runtime_config.py +4 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/sdk_config.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/source.py +6 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/agent_contracts.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/base_contracts.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/errors/error_handler.py +2 -0
- sofias_sdk_lite-0.3.0/src/sofias_sdk_lite/mcp.py +240 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/messaging/models.py +6 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/aggregator_config.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/aggregator_node.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/base_node.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/delegation_config.py +6 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/function_node.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/llm_node.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/llm_node_config.py +4 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/planner_node.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/prompt_assembler.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/events.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/tracing.py +4 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/consumer.py +3 -1
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/delegation_transport.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/rpc_client.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/routing/router.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/routing/strategies.py +14 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/runner/runner.py +3 -1
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/state/conversation_state.py +4 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/state/history.py +2 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/LICENSE +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/execution_context.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/streaming.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/defaults.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/node_contracts.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/strict_contract.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/errors/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/errors/circuit_breaker.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/errors/exceptions.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/factory.py +8 -8
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/gateway.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/openai_compatible.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/protocol.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/tokens.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/tools.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/messaging/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/delegation_node.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/delegation_transport.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/plan_executor.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/planning_models.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/_log.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/trace_context.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/py.typed +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/client.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/config.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/publisher.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/types.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/routing/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/runner/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/runner/config.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/state/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/state/memory.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/tools.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/workflows/__init__.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/workflows/chat.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/workflows/null.py +0 -0
- {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/workflows/streaming_chat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sofias-sdk-lite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Build agents on the Sofias agent graph and run them over RabbitMQ.
|
|
5
5
|
Keywords: agents,ai,llm,rabbitmq,sofias
|
|
6
6
|
Author: Sofias Tech, S.L.
|
|
@@ -56,6 +56,11 @@ Build multi-node agents on a declarative graph and run them against RabbitMQ.
|
|
|
56
56
|
from the agent's settings or the `SOFIAS_LLM_*` environment. `create_llm()`
|
|
57
57
|
builds the client when you need it explicitly; `LLMCallable` stays a protocol
|
|
58
58
|
for anything the bundled clients do not cover.
|
|
59
|
+
- **The user's connectors** — `McpToolProvider` turns the connectors the platform
|
|
60
|
+
sends with each turn (mail, documents, CRM, the company's own MCP servers…)
|
|
61
|
+
into tools for your LLM nodes. They go through the platform's intermediary:
|
|
62
|
+
your agent never sees a credential. See
|
|
63
|
+
[Using the user's connectors](https://docs.sofias.ai/guides/connectors/).
|
|
59
64
|
|
|
60
65
|
Nothing here depends on a private backend or an internal config service.
|
|
61
66
|
Where the model lives is deployment configuration, not agent code.
|
|
@@ -209,11 +214,11 @@ if __name__ == "__main__":
|
|
|
209
214
|
|
|
210
215
|
## Scope (v1)
|
|
211
216
|
|
|
212
|
-
Core agent graph, RabbitMQ messaging, the runner,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
Core agent graph, RabbitMQ messaging, the runner, LLM clients for the Sofias
|
|
218
|
+
gateway / any OpenAI-compatible endpoint and, since 0.3.0, the user's
|
|
219
|
+
connectors (MCP) ship today. Memory is intentionally out of scope for v1 — the
|
|
220
|
+
SDK ships the `MemoryProvider` protocol so you can plug in your own, and it
|
|
221
|
+
becomes an optional extra in a later release.
|
|
217
222
|
|
|
218
223
|
## Source and contributions
|
|
219
224
|
|
|
@@ -20,6 +20,11 @@ Build multi-node agents on a declarative graph and run them against RabbitMQ.
|
|
|
20
20
|
from the agent's settings or the `SOFIAS_LLM_*` environment. `create_llm()`
|
|
21
21
|
builds the client when you need it explicitly; `LLMCallable` stays a protocol
|
|
22
22
|
for anything the bundled clients do not cover.
|
|
23
|
+
- **The user's connectors** — `McpToolProvider` turns the connectors the platform
|
|
24
|
+
sends with each turn (mail, documents, CRM, the company's own MCP servers…)
|
|
25
|
+
into tools for your LLM nodes. They go through the platform's intermediary:
|
|
26
|
+
your agent never sees a credential. See
|
|
27
|
+
[Using the user's connectors](https://docs.sofias.ai/guides/connectors/).
|
|
23
28
|
|
|
24
29
|
Nothing here depends on a private backend or an internal config service.
|
|
25
30
|
Where the model lives is deployment configuration, not agent code.
|
|
@@ -173,11 +178,11 @@ if __name__ == "__main__":
|
|
|
173
178
|
|
|
174
179
|
## Scope (v1)
|
|
175
180
|
|
|
176
|
-
Core agent graph, RabbitMQ messaging, the runner,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
Core agent graph, RabbitMQ messaging, the runner, LLM clients for the Sofias
|
|
182
|
+
gateway / any OpenAI-compatible endpoint and, since 0.3.0, the user's
|
|
183
|
+
connectors (MCP) ship today. Memory is intentionally out of scope for v1 — the
|
|
184
|
+
SDK ships the `MemoryProvider` protocol so you can plug in your own, and it
|
|
185
|
+
becomes an optional extra in a later release.
|
|
181
186
|
|
|
182
187
|
## Source and contributions
|
|
183
188
|
|
|
@@ -67,12 +67,13 @@ class Agent:
|
|
|
67
67
|
to Node instances which handle their own execution.
|
|
68
68
|
|
|
69
69
|
Example:
|
|
70
|
+
```python
|
|
70
71
|
# Agent is typically built via AgentBuilder
|
|
71
72
|
agent = (
|
|
72
73
|
AgentBuilder("my_agent", version="1.0.0")
|
|
73
74
|
.with_settings_class(MySettings)
|
|
74
75
|
.with_contract(input_schema=MyInput, output_schema=MyOutput)
|
|
75
|
-
.
|
|
76
|
+
.add_llm_node("start", node_config, contract)
|
|
76
77
|
.set_entry_node("start")
|
|
77
78
|
.set_terminal("start")
|
|
78
79
|
.build()
|
|
@@ -86,6 +87,7 @@ class Agent:
|
|
|
86
87
|
)
|
|
87
88
|
)
|
|
88
89
|
assert response.status == ResponseStatus.SUCCESS
|
|
90
|
+
```
|
|
89
91
|
"""
|
|
90
92
|
|
|
91
93
|
def __init__(
|
|
@@ -64,6 +64,7 @@ class AgentBuilder:
|
|
|
64
64
|
time, catching configuration errors before runtime.
|
|
65
65
|
|
|
66
66
|
Example:
|
|
67
|
+
```python
|
|
67
68
|
agent = (
|
|
68
69
|
AgentBuilder("inbox_assist", version="1.0.0")
|
|
69
70
|
.with_description("Agent that processes incoming emails")
|
|
@@ -84,6 +85,7 @@ class AgentBuilder:
|
|
|
84
85
|
.with_circuit_breaker(CircuitBreakerConfig(failure_threshold=5))
|
|
85
86
|
.build()
|
|
86
87
|
)
|
|
88
|
+
```
|
|
87
89
|
"""
|
|
88
90
|
|
|
89
91
|
def __init__(self, name: str, *, version: str = "1.0.0") -> None:
|
|
@@ -408,6 +410,7 @@ class AgentBuilder:
|
|
|
408
410
|
Self for method chaining.
|
|
409
411
|
|
|
410
412
|
Example:
|
|
413
|
+
```python
|
|
411
414
|
builder.with_delegation_transport(my_transport).add_delegation_node(
|
|
412
415
|
name="delegate_analysis",
|
|
413
416
|
config=DelegationNodeConfig(
|
|
@@ -424,6 +427,7 @@ class AgentBuilder:
|
|
|
424
427
|
"transform_for_analyzer": lambda d: {"text": d["content"]},
|
|
425
428
|
},
|
|
426
429
|
)
|
|
430
|
+
```
|
|
427
431
|
"""
|
|
428
432
|
self._delegation_node_configs[name] = config
|
|
429
433
|
self._delegation_node_contracts[name] = contract
|
|
@@ -709,6 +713,7 @@ class AgentBuilder:
|
|
|
709
713
|
Self for method chaining.
|
|
710
714
|
|
|
711
715
|
Example:
|
|
716
|
+
```python
|
|
712
717
|
from sofias_sdk_lite.state import (
|
|
713
718
|
ConversationState,
|
|
714
719
|
InMemoryStateProvider,
|
|
@@ -717,6 +722,7 @@ class AgentBuilder:
|
|
|
717
722
|
builder.with_conversation_state(
|
|
718
723
|
ConversationState(InMemoryStateProvider())
|
|
719
724
|
)
|
|
725
|
+
```
|
|
720
726
|
"""
|
|
721
727
|
self._conversation_state = state
|
|
722
728
|
return self
|
|
@@ -47,6 +47,7 @@ class AgentConfig(BaseModel):
|
|
|
47
47
|
three-level configuration resolution: SDK > Agent > Node.
|
|
48
48
|
|
|
49
49
|
Example:
|
|
50
|
+
```python
|
|
50
51
|
config = AgentConfig(
|
|
51
52
|
name="inbox-assistant",
|
|
52
53
|
version="1.0.0",
|
|
@@ -56,6 +57,7 @@ class AgentConfig(BaseModel):
|
|
|
56
57
|
settings_class=InboxAssistantSettings,
|
|
57
58
|
llm=AgentLLMConfig(model="gpt-4o"),
|
|
58
59
|
)
|
|
60
|
+
```
|
|
59
61
|
"""
|
|
60
62
|
|
|
61
63
|
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
|
@@ -4,7 +4,8 @@ Middleware hooks are called before and after each node execution,
|
|
|
4
4
|
allowing logging, metrics, tracing, and input/output transformation
|
|
5
5
|
without modifying node code.
|
|
6
6
|
|
|
7
|
-
Example
|
|
7
|
+
Example:
|
|
8
|
+
```python
|
|
8
9
|
|
|
9
10
|
class MetricsMiddleware:
|
|
10
11
|
async def before_node(self, node_name, input_data):
|
|
@@ -24,6 +25,7 @@ Example::
|
|
|
24
25
|
.with_middleware(MetricsMiddleware())
|
|
25
26
|
.build()
|
|
26
27
|
)
|
|
28
|
+
```
|
|
27
29
|
"""
|
|
28
30
|
|
|
29
31
|
from __future__ import annotations
|
{sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/response_workflow.py
RENAMED
|
@@ -21,6 +21,7 @@ This module is transport-agnostic: it has no dependency on any specific
|
|
|
21
21
|
message broker. Wire it to whatever transport your application uses.
|
|
22
22
|
|
|
23
23
|
Example:
|
|
24
|
+
```python
|
|
24
25
|
class MyRabbitMQWorkflow:
|
|
25
26
|
async def send_response(
|
|
26
27
|
self,
|
|
@@ -38,6 +39,7 @@ Example:
|
|
|
38
39
|
.with_response_workflow(MyRabbitMQWorkflow())
|
|
39
40
|
.build()
|
|
40
41
|
)
|
|
42
|
+
```
|
|
41
43
|
"""
|
|
42
44
|
|
|
43
45
|
from __future__ import annotations
|
|
@@ -99,6 +101,7 @@ class StreamingResponseWorkflow(Protocol):
|
|
|
99
101
|
send_response() is still called at the end with the complete response.
|
|
100
102
|
|
|
101
103
|
Example:
|
|
104
|
+
```python
|
|
102
105
|
class MySSEWorkflow:
|
|
103
106
|
async def on_stream_event(
|
|
104
107
|
self,
|
|
@@ -113,6 +116,7 @@ class StreamingResponseWorkflow(Protocol):
|
|
|
113
116
|
context: ExecutionContext,
|
|
114
117
|
) -> None:
|
|
115
118
|
await self._sse_channel.send_final(response.model_dump_json())
|
|
119
|
+
```
|
|
116
120
|
"""
|
|
117
121
|
|
|
118
122
|
async def on_stream_event(
|
|
@@ -162,7 +166,8 @@ class BaseDelegationResponseWorkflow:
|
|
|
162
166
|
the ``result`` string. If *None*, the entire content dict is
|
|
163
167
|
serialized as JSON. Defaults to *None*.
|
|
164
168
|
|
|
165
|
-
Example
|
|
169
|
+
Example:
|
|
170
|
+
```python
|
|
166
171
|
|
|
167
172
|
async def publish(payload: dict) -> None:
|
|
168
173
|
await my_transport.publish_reply(reply_to, correlation_id, payload)
|
|
@@ -171,6 +176,7 @@ class BaseDelegationResponseWorkflow:
|
|
|
171
176
|
publish_fn=publish,
|
|
172
177
|
output_field="report",
|
|
173
178
|
)
|
|
179
|
+
```
|
|
174
180
|
"""
|
|
175
181
|
|
|
176
182
|
def __init__(
|
{sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/agent_settings.py
RENAMED
|
@@ -55,12 +55,13 @@ class BaseAgentSettings(AgentSettings):
|
|
|
55
55
|
variables), so a ``{api_key}`` placeholder can never inline a secret. Read
|
|
56
56
|
them at the point of use with ``.get_secret_value()``.
|
|
57
57
|
|
|
58
|
-
Example
|
|
59
|
-
|
|
58
|
+
Example:
|
|
59
|
+
```python
|
|
60
60
|
class MySettings(BaseAgentSettings):
|
|
61
61
|
CREDENTIAL_FIELDS = BaseAgentSettings.CREDENTIAL_FIELDS | {"search_api_key"}
|
|
62
62
|
|
|
63
63
|
search_api_key: SecretStr = SecretStr("")
|
|
64
|
+
```
|
|
64
65
|
"""
|
|
65
66
|
|
|
66
67
|
# --- LLM gateway (canonical platform keys) ---
|
{sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/runtime_config.py
RENAMED
|
@@ -38,11 +38,13 @@ class AgentSettings(BaseModel):
|
|
|
38
38
|
required and optional runtime configuration variables.
|
|
39
39
|
|
|
40
40
|
Example:
|
|
41
|
+
```python
|
|
41
42
|
class MyAgentSettings(AgentSettings):
|
|
42
43
|
reasoning_model: str # required
|
|
43
44
|
fast_model: str # required
|
|
44
45
|
max_retries: int = 3 # optional with default
|
|
45
46
|
temperature: float = 0.7 # optional with default
|
|
47
|
+
```
|
|
46
48
|
"""
|
|
47
49
|
|
|
48
50
|
model_config = ConfigDict(
|
|
@@ -85,6 +87,7 @@ class SettingsResolver(Generic[SettingsT]):
|
|
|
85
87
|
graph execution without explicit parameter passing.
|
|
86
88
|
|
|
87
89
|
Example:
|
|
90
|
+
```python
|
|
88
91
|
resolver = SettingsResolver(MyAgentSettings)
|
|
89
92
|
|
|
90
93
|
# Resolve from incoming message
|
|
@@ -98,6 +101,7 @@ class SettingsResolver(Generic[SettingsT]):
|
|
|
98
101
|
|
|
99
102
|
# Clear after execution
|
|
100
103
|
resolver.clear()
|
|
104
|
+
```
|
|
101
105
|
"""
|
|
102
106
|
|
|
103
107
|
def __init__(self, settings_class: type[SettingsT]) -> None:
|
|
@@ -73,10 +73,12 @@ class SDKConfig(BaseModel):
|
|
|
73
73
|
when setting up their agent. All values have sensible defaults.
|
|
74
74
|
|
|
75
75
|
Example:
|
|
76
|
+
```python
|
|
76
77
|
config = SDKConfig(
|
|
77
78
|
llm=LLMConfig(model="gpt-4o-mini"),
|
|
78
79
|
strict_validation=True,
|
|
79
80
|
)
|
|
81
|
+
```
|
|
80
82
|
"""
|
|
81
83
|
|
|
82
84
|
model_config = ConfigDict(frozen=True)
|
|
@@ -35,11 +35,13 @@ class ConfigSource(Protocol):
|
|
|
35
35
|
return type.
|
|
36
36
|
|
|
37
37
|
Example:
|
|
38
|
+
```python
|
|
38
39
|
class DatabaseConfigSource:
|
|
39
40
|
async def fetch(
|
|
40
41
|
self, agent_name: str, conversation_id: str | None = None
|
|
41
42
|
) -> dict[str, Any]:
|
|
42
43
|
return await my_db.get_agent_config(agent_name)
|
|
44
|
+
```
|
|
43
45
|
"""
|
|
44
46
|
|
|
45
47
|
async def fetch(self, agent_name: str, conversation_id: str | None = None) -> dict[str, Any]:
|
|
@@ -66,8 +68,10 @@ class StaticConfigSource:
|
|
|
66
68
|
fixed configuration applies to every agent and conversation.
|
|
67
69
|
|
|
68
70
|
Example:
|
|
71
|
+
```python
|
|
69
72
|
source = StaticConfigSource({"model_name": "gpt-4o-mini", "api_key": "..."})
|
|
70
73
|
config = await source.fetch("my_agent")
|
|
74
|
+
```
|
|
71
75
|
"""
|
|
72
76
|
|
|
73
77
|
def __init__(self, config: dict[str, Any]) -> None:
|
|
@@ -104,8 +108,10 @@ class EnvConfigSource:
|
|
|
104
108
|
export SOFIAS_AGENT_CONFIG='{"model_name": "gpt-4o-mini", "api_key": "sk-..."}'
|
|
105
109
|
|
|
106
110
|
Example:
|
|
111
|
+
```python
|
|
107
112
|
source = EnvConfigSource() # reads SOFIAS_AGENT_CONFIG
|
|
108
113
|
config = await source.fetch("my_agent")
|
|
114
|
+
```
|
|
109
115
|
"""
|
|
110
116
|
|
|
111
117
|
def __init__(self, env_var: str = "SOFIAS_AGENT_CONFIG") -> None:
|
{sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/agent_contracts.py
RENAMED
|
@@ -8,6 +8,7 @@ strongly-typed envelopes that carry domain data plus transport metadata.
|
|
|
8
8
|
- AgentResponse wraps the validated output with execution metadata.
|
|
9
9
|
|
|
10
10
|
Example:
|
|
11
|
+
```python
|
|
11
12
|
class MyInput(InputContract):
|
|
12
13
|
query: str
|
|
13
14
|
|
|
@@ -22,6 +23,7 @@ Example:
|
|
|
22
23
|
print(response.content) # validated output dict
|
|
23
24
|
print(response.execution_path) # ["classifier", "responder"]
|
|
24
25
|
print(response.execution_time_ms) # 1234.5
|
|
26
|
+
```
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
29
|
from __future__ import annotations
|
{sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/base_contracts.py
RENAMED
|
@@ -6,6 +6,7 @@ a common protocol so that any delegating node knows exactly what to send and
|
|
|
6
6
|
what to expect back, without needing to know the internals of the target agent.
|
|
7
7
|
|
|
8
8
|
Example:
|
|
9
|
+
```python
|
|
9
10
|
# Agent A delegates to Agent B
|
|
10
11
|
request = DelegationRequest(
|
|
11
12
|
input="Summarize this document",
|
|
@@ -18,6 +19,7 @@ Example:
|
|
|
18
19
|
result="The document discusses...",
|
|
19
20
|
extras={"word_count": 150, "confidence": 0.95},
|
|
20
21
|
)
|
|
22
|
+
```
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
25
|
from __future__ import annotations
|
|
@@ -153,12 +153,14 @@ class ErrorHandler:
|
|
|
153
153
|
callable provided by the agent.
|
|
154
154
|
|
|
155
155
|
Example:
|
|
156
|
+
```python
|
|
156
157
|
handler = ErrorHandler(config=config, store=store)
|
|
157
158
|
result = await handler.handle_node_execution(
|
|
158
159
|
node_name="processor",
|
|
159
160
|
execute_fn=node.execute,
|
|
160
161
|
input_data={"query": "hello"},
|
|
161
162
|
)
|
|
163
|
+
```
|
|
162
164
|
"""
|
|
163
165
|
|
|
164
166
|
def __init__(
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""MCP connectors delivered by the Sofias platform.
|
|
2
|
+
|
|
3
|
+
The platform sends each turn's connectors in ``AgentTaskMessage.mcp_servers``.
|
|
4
|
+
Inside ``configuration`` it puts the transport details under ``__mcp_*`` keys:
|
|
5
|
+
|
|
6
|
+
- ``__mcp_url``: the endpoint to call. For marketplace agents it is always the
|
|
7
|
+
platform's MCP intermediary, which adds the user's real credentials itself.
|
|
8
|
+
- ``__mcp_headers``: headers for every request (a short-lived grant). Never put
|
|
9
|
+
them in a prompt or a log.
|
|
10
|
+
- ``__mcp_tools``: the allow list approved for that connector.
|
|
11
|
+
- ``__mcp_namespace``: offer the tools as ``connector__tool`` so two connectors
|
|
12
|
+
with a tool of the same name do not clash.
|
|
13
|
+
|
|
14
|
+
``McpToolProvider`` implements the ``ToolProvider`` protocol on top of those
|
|
15
|
+
entries with a minimal Streamable HTTP client (JSON-RPC over ``httpx``, JSON or
|
|
16
|
+
event-stream answers, ``Mcp-Session-Id``), so it needs no extra dependency::
|
|
17
|
+
|
|
18
|
+
from sofias_sdk_lite.mcp import McpToolProvider
|
|
19
|
+
from sofias_sdk_lite.tools import ToolRegistry
|
|
20
|
+
|
|
21
|
+
provider = McpToolProvider(task.mcp_servers)
|
|
22
|
+
builder.with_tool_registry(ToolRegistry(provider))
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
import httpx
|
|
33
|
+
|
|
34
|
+
from sofias_sdk_lite.llm.tools import ToolSpec
|
|
35
|
+
from sofias_sdk_lite.observability._log import get_logger
|
|
36
|
+
|
|
37
|
+
logger = get_logger("mcp")
|
|
38
|
+
|
|
39
|
+
__all__ = ["McpError", "McpServer", "McpToolProvider", "mcp_servers_from_message"]
|
|
40
|
+
|
|
41
|
+
PROTOCOL_VERSION = "2025-06-18"
|
|
42
|
+
_UNSUPPORTED = re.compile(r"[^A-Za-z0-9_-]")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class McpError(Exception):
|
|
46
|
+
"""An MCP server answered with an error or could not be reached."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class McpServer:
|
|
51
|
+
"""One connector of the turn, ready to call."""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
url: str
|
|
55
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
56
|
+
tools: tuple[str, ...] | None = None
|
|
57
|
+
namespace: bool = False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _field(raw: Any, name: str) -> Any:
|
|
61
|
+
return raw.get(name) if isinstance(raw, dict) else getattr(raw, name, None)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def mcp_servers_from_message(raw_servers: list[Any]) -> list[McpServer]:
|
|
65
|
+
"""Read the ``mcp_servers`` of a task message (models or plain dicts)."""
|
|
66
|
+
servers: list[McpServer] = []
|
|
67
|
+
for raw in raw_servers:
|
|
68
|
+
conf = dict(_field(raw, "conf") or _field(raw, "configuration") or {})
|
|
69
|
+
tools = conf.get("__mcp_tools")
|
|
70
|
+
servers.append(
|
|
71
|
+
McpServer(
|
|
72
|
+
name=str(_field(raw, "name")),
|
|
73
|
+
url=conf.get("__mcp_url") or f"http://{_field(raw, 'address')}:{_field(raw, 'port')}/mcp",
|
|
74
|
+
headers=dict(conf.get("__mcp_headers") or {}),
|
|
75
|
+
tools=tuple(tools) if tools else None,
|
|
76
|
+
namespace=bool(conf.get("__mcp_namespace") or conf.get("__mcp_headers")),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
return servers
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _offered_name(server: McpServer, tool: str) -> str:
|
|
83
|
+
if not server.namespace:
|
|
84
|
+
return tool
|
|
85
|
+
return _UNSUPPORTED.sub("_", f"{server.name}__{tool}")[:64]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _Session:
|
|
89
|
+
"""A minimal Streamable HTTP MCP session against one server."""
|
|
90
|
+
|
|
91
|
+
def __init__(self, server: McpServer, client: httpx.AsyncClient) -> None:
|
|
92
|
+
self._server = server
|
|
93
|
+
self._client = client
|
|
94
|
+
self._session_id: str | None = None
|
|
95
|
+
self._next_id = 0
|
|
96
|
+
self._initialized = False
|
|
97
|
+
|
|
98
|
+
async def _post(self, payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
99
|
+
headers = {
|
|
100
|
+
**self._server.headers,
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
"Accept": "application/json, text/event-stream",
|
|
103
|
+
"MCP-Protocol-Version": PROTOCOL_VERSION,
|
|
104
|
+
}
|
|
105
|
+
if self._session_id:
|
|
106
|
+
headers["Mcp-Session-Id"] = self._session_id
|
|
107
|
+
try:
|
|
108
|
+
response = await self._client.post(self._server.url, json=payload, headers=headers)
|
|
109
|
+
except httpx.HTTPError as exc:
|
|
110
|
+
raise McpError(f"{self._server.name}: unreachable ({type(exc).__name__})") from exc
|
|
111
|
+
self._session_id = response.headers.get("Mcp-Session-Id") or self._session_id
|
|
112
|
+
if response.status_code == 202 or "id" not in payload:
|
|
113
|
+
return None
|
|
114
|
+
if response.status_code >= 400:
|
|
115
|
+
raise McpError(f"{self._server.name}: HTTP {response.status_code}")
|
|
116
|
+
message = self._message(response, payload["id"])
|
|
117
|
+
if message.get("error"):
|
|
118
|
+
raise McpError(f"{self._server.name}: {message['error'].get('message', message['error'])}")
|
|
119
|
+
return message.get("result") or {}
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _message(response: httpx.Response, id_: int) -> dict[str, Any]:
|
|
123
|
+
if "text/event-stream" not in response.headers.get("Content-Type", ""):
|
|
124
|
+
return response.json()
|
|
125
|
+
messages = []
|
|
126
|
+
for event in re.split(r"\r?\n\r?\n", response.text):
|
|
127
|
+
data = "\n".join(line[5:].strip() for line in event.splitlines() if line.startswith("data:"))
|
|
128
|
+
if data:
|
|
129
|
+
try:
|
|
130
|
+
messages.append(json.loads(data))
|
|
131
|
+
except json.JSONDecodeError:
|
|
132
|
+
continue
|
|
133
|
+
for message in messages:
|
|
134
|
+
if message.get("id") == id_:
|
|
135
|
+
return message
|
|
136
|
+
raise McpError("event stream without an answer")
|
|
137
|
+
|
|
138
|
+
async def request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
139
|
+
if not self._initialized:
|
|
140
|
+
await self._initialize()
|
|
141
|
+
return await self._call(method, params) or {}
|
|
142
|
+
|
|
143
|
+
async def _call(self, method: str, params: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
144
|
+
self._next_id += 1
|
|
145
|
+
payload: dict[str, Any] = {"jsonrpc": "2.0", "id": self._next_id, "method": method}
|
|
146
|
+
if params is not None:
|
|
147
|
+
payload["params"] = params
|
|
148
|
+
return await self._post(payload)
|
|
149
|
+
|
|
150
|
+
async def _initialize(self) -> None:
|
|
151
|
+
await self._call(
|
|
152
|
+
"initialize",
|
|
153
|
+
{"protocolVersion": PROTOCOL_VERSION, "capabilities": {},
|
|
154
|
+
"clientInfo": {"name": "sofias-sdk-lite", "version": "1"}},
|
|
155
|
+
)
|
|
156
|
+
await self._post({"jsonrpc": "2.0", "method": "notifications/initialized"})
|
|
157
|
+
self._initialized = True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class McpToolProvider:
|
|
161
|
+
"""``ToolProvider`` over the connectors the platform sent for this turn.
|
|
162
|
+
|
|
163
|
+
Connects lazily on the first ``get_tools``. A connector that cannot be
|
|
164
|
+
reached is skipped (and logged) so the others keep working.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, raw_servers: list[Any], *, transport: httpx.AsyncBaseTransport | None = None,
|
|
168
|
+
timeout: float = 60.0) -> None:
|
|
169
|
+
self._servers = mcp_servers_from_message(raw_servers)
|
|
170
|
+
self._client = httpx.AsyncClient(transport=transport, timeout=timeout)
|
|
171
|
+
self._sessions = {server.name: _Session(server, self._client) for server in self._servers}
|
|
172
|
+
self._routes: dict[str, tuple[str, str]] = {}
|
|
173
|
+
self._specs: list[ToolSpec] | None = None
|
|
174
|
+
|
|
175
|
+
async def get_tools(self, node_name: str) -> list[ToolSpec]:
|
|
176
|
+
if self._specs is None:
|
|
177
|
+
self._specs = []
|
|
178
|
+
for server in self._servers:
|
|
179
|
+
try:
|
|
180
|
+
listed = await self._list(server)
|
|
181
|
+
except McpError as exc:
|
|
182
|
+
logger.warning("mcp_server_unavailable", server=server.name, error=str(exc))
|
|
183
|
+
continue
|
|
184
|
+
self._specs.extend(listed)
|
|
185
|
+
return list(self._specs)
|
|
186
|
+
|
|
187
|
+
async def _list(self, server: McpServer) -> list[ToolSpec]:
|
|
188
|
+
specs: list[ToolSpec] = []
|
|
189
|
+
cursor: str | None = None
|
|
190
|
+
while True:
|
|
191
|
+
result = await self._sessions[server.name].request("tools/list", {"cursor": cursor} if cursor else None)
|
|
192
|
+
for tool in result.get("tools", []):
|
|
193
|
+
name = tool.get("name")
|
|
194
|
+
if not name or (server.tools is not None and name not in server.tools):
|
|
195
|
+
continue
|
|
196
|
+
offered = _offered_name(server, name)
|
|
197
|
+
self._routes[offered] = (server.name, name)
|
|
198
|
+
specs.append(ToolSpec(name=offered, description=tool.get("description") or name,
|
|
199
|
+
parameters_schema=tool.get("inputSchema") or {"type": "object"}))
|
|
200
|
+
cursor = result.get("nextCursor")
|
|
201
|
+
if not cursor:
|
|
202
|
+
return specs
|
|
203
|
+
|
|
204
|
+
async def execute_tool(self, tool_name: str, tool_input: dict[str, Any]) -> Any:
|
|
205
|
+
if self._specs is None:
|
|
206
|
+
await self.get_tools("")
|
|
207
|
+
route = self._routes.get(tool_name)
|
|
208
|
+
if route is None:
|
|
209
|
+
raise McpError(f"unknown MCP tool '{tool_name}'")
|
|
210
|
+
server_name, real_name = route
|
|
211
|
+
result = await self._sessions[server_name].request(
|
|
212
|
+
"tools/call", {"name": real_name, "arguments": tool_input or {}}
|
|
213
|
+
)
|
|
214
|
+
if result.get("isError"):
|
|
215
|
+
raise McpError(_text(result) or f"{tool_name} failed")
|
|
216
|
+
return _with_structured(result)
|
|
217
|
+
|
|
218
|
+
async def aclose(self) -> None:
|
|
219
|
+
await self._client.aclose()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _text(result: dict[str, Any]) -> str:
|
|
223
|
+
return "\n".join(block.get("text", "") for block in result.get("content", []) if block.get("type") == "text")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _with_structured(result: dict[str, Any]) -> str:
|
|
227
|
+
"""The text of a tool result plus its ``structuredContent``, unless the text
|
|
228
|
+
already is that same JSON. Some servers only put a summary in the text."""
|
|
229
|
+
text = _text(result)
|
|
230
|
+
structured = result.get("structuredContent")
|
|
231
|
+
if not structured:
|
|
232
|
+
return text if result.get("content") else json.dumps(result)
|
|
233
|
+
for block in result.get("content", []):
|
|
234
|
+
try:
|
|
235
|
+
if json.loads(block.get("text", "")) == structured:
|
|
236
|
+
return text
|
|
237
|
+
except (TypeError, ValueError):
|
|
238
|
+
continue
|
|
239
|
+
serialized = json.dumps(structured)
|
|
240
|
+
return f"{text}\n{serialized}" if text else serialized
|
|
@@ -332,6 +332,7 @@ class DelegationTarget(BaseModel):
|
|
|
332
332
|
the communication with a specific target agent.
|
|
333
333
|
|
|
334
334
|
Example:
|
|
335
|
+
```python
|
|
335
336
|
target = DelegationTarget(
|
|
336
337
|
agent_name="summarizer",
|
|
337
338
|
routing_key="agents.summarizer.tasks",
|
|
@@ -339,6 +340,7 @@ class DelegationTarget(BaseModel):
|
|
|
339
340
|
output_contract=SummaryOutput,
|
|
340
341
|
timeout_seconds=120,
|
|
341
342
|
)
|
|
343
|
+
```
|
|
342
344
|
|
|
343
345
|
.. note::
|
|
344
346
|
Unlike the internal platform, this SDK does **not** assume any
|
|
@@ -394,12 +396,14 @@ class DelegationStep(BaseModel):
|
|
|
394
396
|
on other steps. Used when dynamic targets are enabled.
|
|
395
397
|
|
|
396
398
|
Example:
|
|
399
|
+
```python
|
|
397
400
|
step = DelegationStep(
|
|
398
401
|
id="step_1",
|
|
399
402
|
agent_name="web-search",
|
|
400
403
|
input={"query": "Bitcoin price"},
|
|
401
404
|
depends_on=[],
|
|
402
405
|
)
|
|
406
|
+
```
|
|
403
407
|
"""
|
|
404
408
|
|
|
405
409
|
model_config = {"frozen": True}
|
|
@@ -445,6 +449,7 @@ class DelegationPlan(BaseModel):
|
|
|
445
449
|
delegation node running in DAG mode with dynamic targets.
|
|
446
450
|
|
|
447
451
|
Example:
|
|
452
|
+
```python
|
|
448
453
|
plan = DelegationPlan(
|
|
449
454
|
steps=[
|
|
450
455
|
DelegationStep(id="s1", agent_name="search", input={"q": "A"}),
|
|
@@ -452,6 +457,7 @@ class DelegationPlan(BaseModel):
|
|
|
452
457
|
DelegationStep(id="s3", agent_name="synthesizer", input={}, depends_on=["s1", "s2"]),
|
|
453
458
|
]
|
|
454
459
|
)
|
|
460
|
+
```
|
|
455
461
|
"""
|
|
456
462
|
|
|
457
463
|
model_config = {"frozen": True}
|