sofias-sdk-lite 0.1.2__tar.gz → 0.1.4__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.1.2 → sofias_sdk_lite-0.1.4}/PKG-INFO +61 -11
  2. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/README.md +57 -10
  3. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/pyproject.toml +9 -2
  4. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/pyproject.toml.orig +9 -2
  5. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/__init__.py +35 -2
  6. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/agent/__init__.py +2 -0
  7. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/agent/agent_builder.py +28 -4
  8. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/agent/execution_context.py +44 -1
  9. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/config/__init__.py +4 -0
  10. sofias_sdk_lite-0.1.4/src/sofias_sdk_lite/config/agent_settings.py +112 -0
  11. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/config/defaults.py +15 -0
  12. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/errors/__init__.py +6 -0
  13. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/errors/exceptions.py +60 -0
  14. sofias_sdk_lite-0.1.4/src/sofias_sdk_lite/llm/__init__.py +72 -0
  15. sofias_sdk_lite-0.1.4/src/sofias_sdk_lite/llm/bifrost.py +135 -0
  16. sofias_sdk_lite-0.1.4/src/sofias_sdk_lite/llm/factory.py +297 -0
  17. sofias_sdk_lite-0.1.4/src/sofias_sdk_lite/llm/openai_compatible.py +522 -0
  18. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/llm/protocol.py +14 -6
  19. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/llm_node.py +334 -64
  20. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/llm_node_config.py +11 -0
  21. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/observability/__init__.py +10 -0
  22. sofias_sdk_lite-0.1.4/src/sofias_sdk_lite/observability/trace_context.py +134 -0
  23. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/rabbitmq/__init__.py +2 -1
  24. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/rabbitmq/consumer.py +13 -1
  25. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/rabbitmq/publisher.py +19 -5
  26. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/rabbitmq/types.py +25 -0
  27. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/runner/runner.py +151 -4
  28. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/state/__init__.py +7 -1
  29. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/state/history.py +31 -1
  30. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/workflows/__init__.py +8 -3
  31. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/workflows/chat.py +52 -12
  32. sofias_sdk_lite-0.1.4/src/sofias_sdk_lite/workflows/streaming_chat.py +320 -0
  33. sofias_sdk_lite-0.1.2/src/sofias_sdk_lite/config/agent_settings.py +0 -72
  34. sofias_sdk_lite-0.1.2/src/sofias_sdk_lite/llm/__init__.py +0 -27
  35. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/agent/agent.py +0 -0
  36. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/agent/agent_config.py +0 -0
  37. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/agent/middleware.py +0 -0
  38. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/agent/response_workflow.py +0 -0
  39. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/agent/streaming.py +0 -0
  40. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/config/runtime_config.py +0 -0
  41. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/config/sdk_config.py +0 -0
  42. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/config/source.py +0 -0
  43. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/contracts/__init__.py +0 -0
  44. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/contracts/agent_contracts.py +0 -0
  45. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/contracts/base_contracts.py +0 -0
  46. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/contracts/node_contracts.py +0 -0
  47. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/contracts/strict_contract.py +0 -0
  48. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/errors/circuit_breaker.py +0 -0
  49. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/errors/error_handler.py +0 -0
  50. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/llm/tokens.py +0 -0
  51. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/llm/tools.py +0 -0
  52. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/messaging/__init__.py +0 -0
  53. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/messaging/models.py +0 -0
  54. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/__init__.py +0 -0
  55. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/aggregator_config.py +0 -0
  56. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/aggregator_node.py +0 -0
  57. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/base_node.py +0 -0
  58. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/delegation_config.py +0 -0
  59. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/delegation_node.py +0 -0
  60. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/delegation_transport.py +0 -0
  61. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/function_node.py +0 -0
  62. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/plan_executor.py +0 -0
  63. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/planner_node.py +0 -0
  64. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/planning_models.py +0 -0
  65. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/nodes/prompt_assembler.py +0 -0
  66. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/observability/_log.py +0 -0
  67. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/observability/events.py +0 -0
  68. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/observability/tracing.py +0 -0
  69. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/py.typed +0 -0
  70. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/rabbitmq/client.py +0 -0
  71. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/rabbitmq/config.py +0 -0
  72. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/rabbitmq/delegation_transport.py +0 -0
  73. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/rabbitmq/rpc_client.py +0 -0
  74. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/routing/__init__.py +0 -0
  75. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/routing/router.py +0 -0
  76. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/routing/strategies.py +0 -0
  77. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/runner/__init__.py +0 -0
  78. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/runner/config.py +0 -0
  79. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/state/conversation_state.py +0 -0
  80. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/state/memory.py +0 -0
  81. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/tools.py +0 -0
  82. {sofias_sdk_lite-0.1.2 → sofias_sdk_lite-0.1.4}/src/sofias_sdk_lite/workflows/null.py +0 -0
@@ -1,16 +1,19 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sofias-sdk-lite
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Build agents on the Sofias agent graph and run them over RabbitMQ.
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Dist: pydantic>=2.0
7
7
  Requires-Dist: aio-pika>=9.0.0
8
8
  Requires-Dist: rstream>=1.0.0
9
9
  Requires-Dist: nh3>=0.3.6
10
+ Requires-Dist: httpx>=0.27.0
10
11
  Requires-Dist: mkdocs-material>=9.5 ; extra == 'docs'
11
12
  Requires-Dist: mkdocstrings[python]>=0.25 ; extra == 'docs'
13
+ Requires-Dist: opentelemetry-api>=1.20.0 ; extra == 'otel'
12
14
  Requires-Python: >=3.11
13
15
  Provides-Extra: docs
16
+ Provides-Extra: otel
14
17
  Description-Content-Type: text/markdown
15
18
 
16
19
  # sofias-sdk-lite
@@ -28,12 +31,14 @@ Build multi-node agents on a declarative graph and run them against RabbitMQ.
28
31
  - **A runner** — `AgentRunner` consumes tasks from a queue, resolves per-request
29
32
  settings, executes your agent, and streams the response back — with graceful
30
33
  shutdown, retries, and circuit breakers built in.
31
- - **Bring your own LLM** — `LLMCallable` is a protocol. Wrap any HTTP client,
32
- local model, or provider SDK in ~15 lines; no vendor lock-in.
34
+ - **An LLM that is already wired** — LLM nodes talk to the Sofias Bifrost
35
+ gateway (or any OpenAI-compatible endpoint) using the model, URL and key
36
+ from the agent's settings or the `SOFIAS_LLM_*` environment. `create_llm()`
37
+ builds the client when you need it explicitly; `LLMCallable` stays a protocol
38
+ for anything the bundled clients do not cover.
33
39
 
34
- Nothing here depends on a private backend, an internal config service, or a
35
- specific LLM vendor. Everything infrastructure-specific is a constructor
36
- argument or a protocol you implement yourself.
40
+ Nothing here depends on a private backend or an internal config service.
41
+ Where the model lives is deployment configuration, not agent code.
37
42
 
38
43
  ## Install
39
44
 
@@ -103,6 +108,44 @@ See [`examples/`](examples/) for a runner against a local RabbitMQ
103
108
  (`docker-compose.yml` included), tool loops, streaming, and agent-to-agent
104
109
  delegation.
105
110
 
111
+ ## Connecting to the LLM
112
+
113
+ An agent declares *what* the model should do; *where* the model lives is
114
+ resolved at `build()`:
115
+
116
+ 1. Under `AgentRunner`, from the per-task settings the platform sends
117
+ (`model_name`, `router_url`, `router_api_key` on `BaseAgentSettings`).
118
+ 2. Otherwise from the environment: `SOFIAS_LLM_MODEL`, `SOFIAS_LLM_BASE_URL`
119
+ (default `http://bifrost:8080/v1`), `SOFIAS_LLM_API_KEY`,
120
+ `SOFIAS_LLM_PROVIDER` (default `bifrost`). The `BIFROST_MODEL` /
121
+ `BIFROST_BASE_URL` / `BIFROST_API_KEY` variables the Sofias Developer
122
+ Portal injects into agent containers are accepted as fallbacks, so a
123
+ portal-deployed agent needs no LLM configuration at all.
124
+ 3. Or explicitly: `.with_llm(create_llm(model="default", api_key=...))`.
125
+
126
+ ```python
127
+ from sofias_sdk_lite import AgentBuilder, LLMNodeConfig, NodeContract
128
+
129
+ agent = (
130
+ AgentBuilder("qa_agent")
131
+ .with_settings_class(MySettings)
132
+ .with_contract(input_schema=Question, output_schema=Answer)
133
+ .add_llm_node(
134
+ "answerer",
135
+ LLMNodeConfig(name="answerer", input_contract=Question, output_contract=Answer,
136
+ system_prompt="Answer in one sentence."),
137
+ NodeContract(input_schema=Question, output_schema=Answer),
138
+ )
139
+ .set_entry_node("answerer")
140
+ .set_terminal("answerer")
141
+ .build() # no LLM client anywhere in this file
142
+ )
143
+ ```
144
+
145
+ See `examples/03_llm_agent.py` and the
146
+ [LLM integration](docs/concepts/llm-integration.md) guide (streaming,
147
+ retries, bringing your own client).
148
+
106
149
  ## Running an agent against RabbitMQ
107
150
 
108
151
  ```python
@@ -138,13 +181,20 @@ if __name__ == "__main__":
138
181
  ).run()
139
182
  ```
140
183
 
184
+ ## Optional extras
185
+
186
+ - `sofias-sdk-lite[otel]` — installs `opentelemetry-api` so the runner opens
187
+ an `agent.handle` span per turn and response fragments carry the active
188
+ span's `traceparent`. Without it, the inbound `traceparent` is still echoed
189
+ end to end.
190
+
141
191
  ## Scope (v1)
142
192
 
143
- Core agent graph, RabbitMQ messaging, and the runner ship today. Memory,
144
- MCP tool discovery, and LLM adapter implementations are intentionally out
145
- of scope for v1 — the SDK ships the relevant protocols
146
- (`MemoryProvider`, `ToolProvider`, `LLMCallable`) so you can plug in your
147
- own, and these become optional extras in a later release.
193
+ Core agent graph, RabbitMQ messaging, the runner, and LLM clients for the
194
+ Bifrost gateway / any OpenAI-compatible endpoint ship today. Memory and MCP
195
+ tool discovery are intentionally out of scope for v1 — the SDK ships the
196
+ relevant protocols (`MemoryProvider`, `ToolProvider`) so you can plug in
197
+ your own, and these become optional extras in a later release.
148
198
 
149
199
  ## License
150
200
 
@@ -13,12 +13,14 @@ Build multi-node agents on a declarative graph and run them against RabbitMQ.
13
13
  - **A runner** — `AgentRunner` consumes tasks from a queue, resolves per-request
14
14
  settings, executes your agent, and streams the response back — with graceful
15
15
  shutdown, retries, and circuit breakers built in.
16
- - **Bring your own LLM** — `LLMCallable` is a protocol. Wrap any HTTP client,
17
- local model, or provider SDK in ~15 lines; no vendor lock-in.
16
+ - **An LLM that is already wired** — LLM nodes talk to the Sofias Bifrost
17
+ gateway (or any OpenAI-compatible endpoint) using the model, URL and key
18
+ from the agent's settings or the `SOFIAS_LLM_*` environment. `create_llm()`
19
+ builds the client when you need it explicitly; `LLMCallable` stays a protocol
20
+ for anything the bundled clients do not cover.
18
21
 
19
- Nothing here depends on a private backend, an internal config service, or a
20
- specific LLM vendor. Everything infrastructure-specific is a constructor
21
- argument or a protocol you implement yourself.
22
+ Nothing here depends on a private backend or an internal config service.
23
+ Where the model lives is deployment configuration, not agent code.
22
24
 
23
25
  ## Install
24
26
 
@@ -88,6 +90,44 @@ See [`examples/`](examples/) for a runner against a local RabbitMQ
88
90
  (`docker-compose.yml` included), tool loops, streaming, and agent-to-agent
89
91
  delegation.
90
92
 
93
+ ## Connecting to the LLM
94
+
95
+ An agent declares *what* the model should do; *where* the model lives is
96
+ resolved at `build()`:
97
+
98
+ 1. Under `AgentRunner`, from the per-task settings the platform sends
99
+ (`model_name`, `router_url`, `router_api_key` on `BaseAgentSettings`).
100
+ 2. Otherwise from the environment: `SOFIAS_LLM_MODEL`, `SOFIAS_LLM_BASE_URL`
101
+ (default `http://bifrost:8080/v1`), `SOFIAS_LLM_API_KEY`,
102
+ `SOFIAS_LLM_PROVIDER` (default `bifrost`). The `BIFROST_MODEL` /
103
+ `BIFROST_BASE_URL` / `BIFROST_API_KEY` variables the Sofias Developer
104
+ Portal injects into agent containers are accepted as fallbacks, so a
105
+ portal-deployed agent needs no LLM configuration at all.
106
+ 3. Or explicitly: `.with_llm(create_llm(model="default", api_key=...))`.
107
+
108
+ ```python
109
+ from sofias_sdk_lite import AgentBuilder, LLMNodeConfig, NodeContract
110
+
111
+ agent = (
112
+ AgentBuilder("qa_agent")
113
+ .with_settings_class(MySettings)
114
+ .with_contract(input_schema=Question, output_schema=Answer)
115
+ .add_llm_node(
116
+ "answerer",
117
+ LLMNodeConfig(name="answerer", input_contract=Question, output_contract=Answer,
118
+ system_prompt="Answer in one sentence."),
119
+ NodeContract(input_schema=Question, output_schema=Answer),
120
+ )
121
+ .set_entry_node("answerer")
122
+ .set_terminal("answerer")
123
+ .build() # no LLM client anywhere in this file
124
+ )
125
+ ```
126
+
127
+ See `examples/03_llm_agent.py` and the
128
+ [LLM integration](docs/concepts/llm-integration.md) guide (streaming,
129
+ retries, bringing your own client).
130
+
91
131
  ## Running an agent against RabbitMQ
92
132
 
93
133
  ```python
@@ -123,13 +163,20 @@ if __name__ == "__main__":
123
163
  ).run()
124
164
  ```
125
165
 
166
+ ## Optional extras
167
+
168
+ - `sofias-sdk-lite[otel]` — installs `opentelemetry-api` so the runner opens
169
+ an `agent.handle` span per turn and response fragments carry the active
170
+ span's `traceparent`. Without it, the inbound `traceparent` is still echoed
171
+ end to end.
172
+
126
173
  ## Scope (v1)
127
174
 
128
- Core agent graph, RabbitMQ messaging, and the runner ship today. Memory,
129
- MCP tool discovery, and LLM adapter implementations are intentionally out
130
- of scope for v1 — the SDK ships the relevant protocols
131
- (`MemoryProvider`, `ToolProvider`, `LLMCallable`) so you can plug in your
132
- own, and these become optional extras in a later release.
175
+ Core agent graph, RabbitMQ messaging, the runner, and LLM clients for the
176
+ Bifrost gateway / any OpenAI-compatible endpoint ship today. Memory and MCP
177
+ tool discovery are intentionally out of scope for v1 — the SDK ships the
178
+ relevant protocols (`MemoryProvider`, `ToolProvider`) so you can plug in
179
+ your own, and these become optional extras in a later release.
133
180
 
134
181
  ## License
135
182
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sofias-sdk-lite"
3
- version = "0.1.2"
3
+ version = "0.1.4"
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"
@@ -10,9 +10,11 @@ dependencies = [
10
10
  "aio-pika>=9.0.0",
11
11
  "rstream>=1.0.0",
12
12
  "nh3>=0.3.6",
13
+ "httpx>=0.27.0",
13
14
  ]
14
15
 
15
16
  [project.optional-dependencies]
17
+ otel = ["opentelemetry-api>=1.20.0"]
16
18
  docs = [
17
19
  "mkdocs-material>=9.5",
18
20
  "mkdocstrings[python]>=0.25",
@@ -24,7 +26,10 @@ url = "https://pypi.org/simple"
24
26
  default = true
25
27
 
26
28
  [tool.pytest.ini_options]
27
- pythonpath = ["src"]
29
+ pythonpath = [
30
+ "src",
31
+ ".",
32
+ ]
28
33
  addopts = "-ra"
29
34
  asyncio_mode = "auto"
30
35
  markers = ["integration: requires a running RabbitMQ broker"]
@@ -48,4 +53,6 @@ dev = [
48
53
  "pytest-cov>=5.0",
49
54
  "ruff>=0.4.0",
50
55
  "pyright>=1.1.408",
56
+ "opentelemetry-api>=1.20.0",
57
+ "opentelemetry-sdk>=1.20.0",
51
58
  ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sofias-sdk-lite"
3
- version = "0.1.2"
3
+ version = "0.1.4"
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"
@@ -10,9 +10,13 @@ dependencies = [
10
10
  "aio-pika>=9.0.0",
11
11
  "rstream>=1.0.0",
12
12
  "nh3>=0.3.6",
13
+ "httpx>=0.27.0",
13
14
  ]
14
15
 
15
16
  [project.optional-dependencies]
17
+ otel = [
18
+ "opentelemetry-api>=1.20.0",
19
+ ]
16
20
  docs = [
17
21
  "mkdocs-material>=9.5",
18
22
  "mkdocstrings[python]>=0.25",
@@ -34,10 +38,13 @@ dev = [
34
38
  "pytest-cov>=5.0",
35
39
  "ruff>=0.4.0",
36
40
  "pyright>=1.1.408",
41
+ # Exercise the optional OpenTelemetry path (the `[otel]` extra) in tests.
42
+ "opentelemetry-api>=1.20.0",
43
+ "opentelemetry-sdk>=1.20.0",
37
44
  ]
38
45
 
39
46
  [tool.pytest.ini_options]
40
- pythonpath = ["src"]
47
+ pythonpath = ["src", "."]
41
48
  addopts = "-ra"
42
49
  asyncio_mode = "auto"
43
50
  markers = ["integration: requires a running RabbitMQ broker"]
@@ -30,6 +30,8 @@ against RabbitMQ, streaming, delegation, and testing.
30
30
 
31
31
  from __future__ import annotations
32
32
 
33
+ from importlib.metadata import PackageNotFoundError, version
34
+
33
35
  from sofias_sdk_lite.agent import (
34
36
  Agent,
35
37
  AgentBuildError,
@@ -45,6 +47,7 @@ from sofias_sdk_lite.agent import (
45
47
  StreamingResponseWorkflow,
46
48
  get_execution_context,
47
49
  set_execution_context,
50
+ usage_state_from_list,
48
51
  )
49
52
  from sofias_sdk_lite.config import (
50
53
  AgentSettings,
@@ -73,6 +76,9 @@ from sofias_sdk_lite.errors import (
73
76
  AgentSDKError,
74
77
  CircuitBreakerConfig,
75
78
  ConfigSourceError,
79
+ EmptyLLMResponseError,
80
+ LLMConfigurationError,
81
+ LLMRequestError,
76
82
  ContractValidationError,
77
83
  ErrorHandler,
78
84
  ErrorHandlerConfig,
@@ -85,9 +91,14 @@ from sofias_sdk_lite.errors import (
85
91
  ToolExecutionError,
86
92
  )
87
93
  from sofias_sdk_lite.llm import (
94
+ BifrostLLM,
88
95
  LLMCallable,
96
+ LLMGatewayConfig,
89
97
  LLMResponse,
98
+ OpenAICompatibleLLM,
90
99
  StreamableLLMCallable,
100
+ create_llm,
101
+ default_llm,
91
102
  TokenUsage,
92
103
  ToolCall,
93
104
  ToolSpec,
@@ -143,10 +154,20 @@ from sofias_sdk_lite.state import (
143
154
  InMemoryStateProvider,
144
155
  MemoryProvider,
145
156
  Message,
157
+ SummarizingHistoryProvider,
158
+ )
159
+ from sofias_sdk_lite.workflows import (
160
+ BaseStreamingResponseWorkflow,
161
+ ChatResponseWorkflow,
162
+ NullWorkflow,
146
163
  )
147
- from sofias_sdk_lite.workflows import ChatResponseWorkflow, NullWorkflow
148
164
 
149
- __version__ = "0.1.0"
165
+ try:
166
+ # Derived from the installed distribution so it never drifts from
167
+ # pyproject.toml (CI rewrites the version there on tagged releases).
168
+ __version__ = version("sofias-sdk-lite")
169
+ except PackageNotFoundError: # pragma: no cover - source checkout without install
170
+ __version__ = "0.0.0"
150
171
 
151
172
  __all__ = [
152
173
  "__version__",
@@ -161,6 +182,7 @@ __all__ = [
161
182
  "ExecutionContext",
162
183
  "get_execution_context",
163
184
  "set_execution_context",
185
+ "usage_state_from_list",
164
186
  "ResponseWorkflow",
165
187
  "StreamingResponseWorkflow",
166
188
  "BaseDelegationResponseWorkflow",
@@ -196,6 +218,9 @@ __all__ = [
196
218
  "ToolExecutionError",
197
219
  "GraphExecutionError",
198
220
  "ConfigSourceError",
221
+ "EmptyLLMResponseError",
222
+ "LLMConfigurationError",
223
+ "LLMRequestError",
199
224
  "ErrorHandler",
200
225
  "ErrorHandlerConfig",
201
226
  "RetryPolicy",
@@ -204,6 +229,12 @@ __all__ = [
204
229
  "LLMCallable",
205
230
  "StreamableLLMCallable",
206
231
  "LLMResponse",
232
+ # LLM clients + factory
233
+ "BifrostLLM",
234
+ "OpenAICompatibleLLM",
235
+ "LLMGatewayConfig",
236
+ "create_llm",
237
+ "default_llm",
207
238
  "TokenUsage",
208
239
  "ToolCall",
209
240
  "ToolSpec",
@@ -251,12 +282,14 @@ __all__ = [
251
282
  "RunnerConfig",
252
283
  # Workflows
253
284
  "ChatResponseWorkflow",
285
+ "BaseStreamingResponseWorkflow",
254
286
  "NullWorkflow",
255
287
  # State
256
288
  "ConversationStateProvider",
257
289
  "InMemoryStateProvider",
258
290
  "MemoryProvider",
259
291
  "HistoryProvider",
292
+ "SummarizingHistoryProvider",
260
293
  "InMemoryHistory",
261
294
  "Message",
262
295
  ]
@@ -8,6 +8,7 @@ from sofias_sdk_lite.agent.execution_context import (
8
8
  TokenAccumulator,
9
9
  get_execution_context,
10
10
  set_execution_context,
11
+ usage_state_from_list,
11
12
  )
12
13
  from sofias_sdk_lite.agent.middleware import (
13
14
  AgentMiddleware,
@@ -39,6 +40,7 @@ __all__ = [
39
40
  "set_execution_context",
40
41
  "ExecutionContext",
41
42
  "TokenAccumulator",
43
+ "usage_state_from_list",
42
44
  # Builder
43
45
  "AgentBuilder",
44
46
  "AgentBuildError",
@@ -24,6 +24,7 @@ from sofias_sdk_lite.errors.error_handler import (
24
24
  )
25
25
  from sofias_sdk_lite.errors.exceptions import AgentSDKError
26
26
  from sofias_sdk_lite.llm import LLMCallable
27
+ from sofias_sdk_lite.llm.factory import ENV_LLM_MODEL, resolve_default_llm
27
28
  from sofias_sdk_lite.nodes.aggregator_config import AggregatorNodeConfig
28
29
  from sofias_sdk_lite.nodes.base_node import BaseNode
29
30
  from sofias_sdk_lite.nodes.delegation_config import DelegationNodeConfig
@@ -230,9 +231,21 @@ class AgentBuilder:
230
231
  self._output_schema = output_schema
231
232
  return self
232
233
 
234
+ _MISSING_LLM_MESSAGE = (
235
+ "No LLM available for LLM nodes. Either call with_llm(), run the agent through "
236
+ "AgentRunner with model_name/router_url/router_api_key in its settings, or export "
237
+ f"{ENV_LLM_MODEL} (plus SOFIAS_LLM_BASE_URL / SOFIAS_LLM_API_KEY)."
238
+ )
239
+
233
240
  def with_llm(self, llm: LLMCallable) -> AgentBuilder:
234
241
  """Set the LLM callable for all LLM nodes.
235
242
 
243
+ Optional. When omitted, `build()` uses the LLM installed by
244
+ `sofias_sdk_lite.llm.default_llm` (what `AgentRunner` does with the
245
+ per-task settings) or, failing that, one built from the
246
+ ``SOFIAS_LLM_*`` environment variables via
247
+ `sofias_sdk_lite.llm.create_llm`.
248
+
236
249
  Args:
237
250
  llm: LLM implementation for making completions.
238
251
 
@@ -242,6 +255,12 @@ class AgentBuilder:
242
255
  self._llm = llm
243
256
  return self
244
257
 
258
+ def _needs_agent_llm(self) -> bool:
259
+ """Whether any node still to be built relies on the agent-level LLM."""
260
+ if self._llm_node_configs:
261
+ return True
262
+ return any(cfg["llm"] is None for cfg in self._planner_node_configs.values())
263
+
245
264
  def with_agent_llm_config(self, config: AgentLLMConfig) -> AgentBuilder:
246
265
  """Set agent-level LLM configuration.
247
266
 
@@ -899,9 +918,9 @@ class AgentBuilder:
899
918
  )
900
919
  errors.extend(graph_errors)
901
920
 
902
- # LLM must be provided if there are LLM nodes to be built (not pre-built)
921
+ # LLM must be resolvable if there are LLM nodes to be built (not pre-built)
903
922
  if self._llm_node_configs and self._llm is None:
904
- errors.append("LLM not provided but LLM nodes need to be built. Use with_llm() to set the LLM callable.")
923
+ errors.append(self._MISSING_LLM_MESSAGE)
905
924
 
906
925
  # Delegation transport must be provided if there are delegation nodes
907
926
  if self._delegation_node_configs and self._delegation_transport is None:
@@ -941,8 +960,8 @@ class AgentBuilder:
941
960
  for name, config in self._planner_node_configs.items():
942
961
  if config["llm"] is None and self._llm is None:
943
962
  errors.append(
944
- f"PlannerNode '{name}' has no LLM and no agent-level LLM is set. "
945
- "Use with_llm() or provide llm to add_planner_node()."
963
+ f"PlannerNode '{name}' has no LLM and no agent-level LLM could be "
964
+ f"resolved. {self._MISSING_LLM_MESSAGE}"
946
965
  )
947
966
 
948
967
  # Validate available_nodes references
@@ -1016,6 +1035,11 @@ class AgentBuilder:
1016
1035
  # Apply default routes before validation
1017
1036
  self._apply_default_routes()
1018
1037
 
1038
+ # Resolve the agent-level LLM when none was given explicitly: the
1039
+ # runner's per-task default (from settings) first, then the environment.
1040
+ if self._llm is None and self._needs_agent_llm():
1041
+ self._llm = resolve_default_llm()
1042
+
1019
1043
  # Validate configuration
1020
1044
  errors = self._validate()
1021
1045
  if errors:
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  from contextvars import ContextVar
11
11
  from dataclasses import dataclass
12
12
  from datetime import datetime
13
- from typing import TYPE_CHECKING, Any
13
+ from typing import TYPE_CHECKING, Any, cast
14
14
 
15
15
  from pydantic import BaseModel, ConfigDict, Field
16
16
 
@@ -22,6 +22,7 @@ __all__ = [
22
22
  "ExecutionContext",
23
23
  "get_execution_context",
24
24
  "set_execution_context",
25
+ "usage_state_from_list",
25
26
  ]
26
27
 
27
28
 
@@ -136,6 +137,48 @@ class TokenAccumulator:
136
137
  return result
137
138
 
138
139
 
140
+ def _usage_entry(entry: Any, key: str, default: Any = 0) -> Any:
141
+ """Read *key* off a usage entry that may be a dict or an object.
142
+
143
+ ``AgentResponse.usage`` entries are typed as ``dict`` (built by
144
+ ``TokenAccumulator.to_usage_list``), but callers are free to set the
145
+ field directly with plain objects, so both shapes must work.
146
+ """
147
+ if isinstance(entry, dict):
148
+ return cast(dict[str, Any], entry).get(key, default)
149
+ return getattr(entry, key, default)
150
+
151
+
152
+ def _usage_entry_int(entry: Any, key: str) -> int:
153
+ try:
154
+ return int(_usage_entry(entry, key, 0) or 0)
155
+ except (TypeError, ValueError):
156
+ return 0
157
+
158
+
159
+ def usage_state_from_list(usage: list[Any] | None) -> dict[str, Any] | None:
160
+ """Collapse an ``AgentResponse.usage`` list into a single ``StreamFragment``
161
+ ``state.usage`` object, or ``None`` when there is nothing to report.
162
+
163
+ Every response workflow that publishes a terminal fragment should call
164
+ this and merge the result into ``state``: it is the one place a billing
165
+ consumer reads token usage from. Sums tokens across every LLM call in the
166
+ turn (a turn can invoke several models: the main chat call plus e.g. an
167
+ embedding call for RAG), and names the entry with the most completion
168
+ tokens as ``model``, which picks the actual generation call over
169
+ near-zero-completion side calls.
170
+ """
171
+ if not usage:
172
+ return None
173
+
174
+ primary = max(usage, key=lambda e: _usage_entry_int(e, "completion_tokens"))
175
+ return {
176
+ "prompt_tokens": sum(_usage_entry_int(e, "prompt_tokens") for e in usage),
177
+ "completion_tokens": sum(_usage_entry_int(e, "completion_tokens") for e in usage),
178
+ "model": _usage_entry(primary, "model_requested", ""),
179
+ }
180
+
181
+
139
182
  class ExecutionContext(BaseModel):
140
183
  """Structured execution context propagated via ContextVar.
141
184
 
@@ -6,12 +6,14 @@ from sofias_sdk_lite.config.defaults import (
6
6
  DEFAULT_MAX_ITERATIONS,
7
7
  DEFAULT_MAX_RETRIES,
8
8
  DEFAULT_MAX_TOKENS,
9
+ DEFAULT_MAX_TOOL_LOOP_TOKENS,
9
10
  DEFAULT_MODEL,
10
11
  DEFAULT_RETRY_BACKOFF_MULTIPLIER,
11
12
  DEFAULT_RETRY_DELAY_SECONDS,
12
13
  DEFAULT_RETRY_MAX_DELAY_SECONDS,
13
14
  DEFAULT_STRICT_VALIDATION,
14
15
  DEFAULT_TEMPERATURE,
16
+ DEFAULT_TOOL_LOOP_TOKEN_RESERVE,
15
17
  DEFAULT_TOP_P,
16
18
  DEFAULT_VERBOSE_LOGGING,
17
19
  )
@@ -38,12 +40,14 @@ __all__ = [
38
40
  "DEFAULT_MAX_ITERATIONS",
39
41
  "DEFAULT_MAX_RETRIES",
40
42
  "DEFAULT_MAX_TOKENS",
43
+ "DEFAULT_MAX_TOOL_LOOP_TOKENS",
41
44
  "DEFAULT_MODEL",
42
45
  "DEFAULT_RETRY_BACKOFF_MULTIPLIER",
43
46
  "DEFAULT_RETRY_DELAY_SECONDS",
44
47
  "DEFAULT_RETRY_MAX_DELAY_SECONDS",
45
48
  "DEFAULT_STRICT_VALIDATION",
46
49
  "DEFAULT_TEMPERATURE",
50
+ "DEFAULT_TOOL_LOOP_TOKEN_RESERVE",
47
51
  "DEFAULT_TOP_P",
48
52
  "DEFAULT_VERBOSE_LOGGING",
49
53
  # SDK Config