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.
Files changed (82) hide show
  1. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/PKG-INFO +11 -6
  2. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/README.md +10 -5
  3. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/pyproject.toml +1 -1
  4. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/pyproject.toml.orig +1 -1
  5. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/agent.py +3 -1
  6. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/agent_builder.py +6 -0
  7. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/agent_config.py +2 -0
  8. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/middleware.py +3 -1
  9. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/response_workflow.py +7 -1
  10. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/agent_settings.py +3 -2
  11. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/runtime_config.py +4 -0
  12. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/sdk_config.py +2 -0
  13. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/source.py +6 -0
  14. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/agent_contracts.py +2 -0
  15. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/base_contracts.py +2 -0
  16. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/errors/error_handler.py +2 -0
  17. sofias_sdk_lite-0.3.0/src/sofias_sdk_lite/mcp.py +240 -0
  18. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/messaging/models.py +6 -0
  19. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/aggregator_config.py +2 -0
  20. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/aggregator_node.py +2 -0
  21. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/base_node.py +2 -0
  22. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/delegation_config.py +6 -0
  23. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/function_node.py +2 -0
  24. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/llm_node.py +2 -0
  25. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/llm_node_config.py +4 -0
  26. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/planner_node.py +2 -0
  27. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/prompt_assembler.py +2 -0
  28. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/events.py +2 -0
  29. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/tracing.py +4 -0
  30. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/consumer.py +3 -1
  31. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/delegation_transport.py +2 -0
  32. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/rpc_client.py +2 -0
  33. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/routing/router.py +2 -0
  34. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/routing/strategies.py +14 -0
  35. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/runner/runner.py +3 -1
  36. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/state/conversation_state.py +4 -0
  37. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/state/history.py +2 -0
  38. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/LICENSE +0 -0
  39. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/__init__.py +0 -0
  40. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/__init__.py +0 -0
  41. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/execution_context.py +0 -0
  42. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/agent/streaming.py +0 -0
  43. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/__init__.py +0 -0
  44. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/config/defaults.py +0 -0
  45. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/__init__.py +0 -0
  46. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/node_contracts.py +0 -0
  47. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/contracts/strict_contract.py +0 -0
  48. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/errors/__init__.py +0 -0
  49. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/errors/circuit_breaker.py +0 -0
  50. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/errors/exceptions.py +0 -0
  51. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/__init__.py +0 -0
  52. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/factory.py +8 -8
  53. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/gateway.py +0 -0
  54. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/openai_compatible.py +0 -0
  55. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/protocol.py +0 -0
  56. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/tokens.py +0 -0
  57. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/llm/tools.py +0 -0
  58. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/messaging/__init__.py +0 -0
  59. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/__init__.py +0 -0
  60. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/delegation_node.py +0 -0
  61. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/delegation_transport.py +0 -0
  62. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/plan_executor.py +0 -0
  63. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/nodes/planning_models.py +0 -0
  64. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/__init__.py +0 -0
  65. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/_log.py +0 -0
  66. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/observability/trace_context.py +0 -0
  67. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/py.typed +0 -0
  68. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/__init__.py +0 -0
  69. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/client.py +0 -0
  70. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/config.py +0 -0
  71. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/publisher.py +0 -0
  72. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/rabbitmq/types.py +0 -0
  73. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/routing/__init__.py +0 -0
  74. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/runner/__init__.py +0 -0
  75. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/runner/config.py +0 -0
  76. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/state/__init__.py +0 -0
  77. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/state/memory.py +0 -0
  78. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/tools.py +0 -0
  79. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/workflows/__init__.py +0 -0
  80. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/workflows/chat.py +0 -0
  81. {sofias_sdk_lite-0.2.0 → sofias_sdk_lite-0.3.0}/src/sofias_sdk_lite/workflows/null.py +0 -0
  82. {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.2.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, and LLM clients for the
213
- Sofias gateway / any OpenAI-compatible endpoint ship today. Memory and MCP
214
- tool discovery are intentionally out of scope for v1 — the SDK ships the
215
- relevant protocols (`MemoryProvider`, `ToolProvider`) so you can plug in
216
- your own, and these become optional extras in a later release.
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, and LLM clients for the
177
- Sofias gateway / any OpenAI-compatible endpoint ship today. Memory and MCP
178
- tool discovery are intentionally out of scope for v1 — the SDK ships the
179
- relevant protocols (`MemoryProvider`, `ToolProvider`) so you can plug in
180
- your own, and these become optional extras in a later release.
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sofias-sdk-lite"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Build agents on the Sofias agent graph and run them over RabbitMQ."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sofias-sdk-lite"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Build agents on the Sofias agent graph and run them over RabbitMQ."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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
- .add_node("start", node_config, contract)
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
@@ -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__(
@@ -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) ---
@@ -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:
@@ -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
@@ -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}