sofias-sdk-lite 0.3.0__tar.gz → 0.4.1__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.3.0 → sofias_sdk_lite-0.4.1}/PKG-INFO +1 -1
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/pyproject.toml +4 -1
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/pyproject.toml.orig +8 -1
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/__init__.py +11 -0
- sofias_sdk_lite-0.4.1/src/sofias_sdk_lite/files.py +195 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/messaging/models.py +10 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/llm_node.py +63 -9
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/prompt_assembler.py +65 -1
- sofias_sdk_lite-0.4.1/src/sofias_sdk_lite/observability/tool_events.py +80 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/LICENSE +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/README.md +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/agent.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/agent_builder.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/agent_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/execution_context.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/middleware.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/response_workflow.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/streaming.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/agent_settings.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/defaults.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/runtime_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/sdk_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/source.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/agent_contracts.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/base_contracts.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/node_contracts.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/strict_contract.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/errors/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/errors/circuit_breaker.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/errors/error_handler.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/errors/exceptions.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/factory.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/gateway.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/openai_compatible.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/protocol.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/tokens.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/tools.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/mcp.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/messaging/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/aggregator_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/aggregator_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/base_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/delegation_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/delegation_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/delegation_transport.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/function_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/llm_node_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/plan_executor.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/planner_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/planning_models.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/_log.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/events.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/trace_context.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/tracing.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/py.typed +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/client.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/consumer.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/delegation_transport.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/publisher.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/rpc_client.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/types.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/routing/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/routing/router.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/routing/strategies.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/runner/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/runner/config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/runner/runner.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/state/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/state/conversation_state.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/state/history.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/state/memory.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/tools.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/workflows/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/workflows/chat.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/workflows/null.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/workflows/streaming_chat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sofias-sdk-lite"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.1"
|
|
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"
|
|
@@ -68,6 +68,9 @@ markers = ["integration: requires a running RabbitMQ broker"]
|
|
|
68
68
|
line-length = 100
|
|
69
69
|
target-version = "py311"
|
|
70
70
|
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
extend-select = ["TC004"]
|
|
73
|
+
|
|
71
74
|
[tool.pyright]
|
|
72
75
|
include = ["src"]
|
|
73
76
|
typeCheckingMode = "strict"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sofias-sdk-lite"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.1"
|
|
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"
|
|
@@ -75,6 +75,13 @@ markers = ["integration: requires a running RabbitMQ broker"]
|
|
|
75
75
|
line-length = 100
|
|
76
76
|
target-version = "py311"
|
|
77
77
|
|
|
78
|
+
[tool.ruff.lint]
|
|
79
|
+
# TC004 catches a name imported only under ``if TYPE_CHECKING:`` but used at
|
|
80
|
+
# runtime. That shipped in 0.4.0 as a NameError the moment a node executed a
|
|
81
|
+
# caller-owned tool (TKT-067). Neither pyright strict nor mypy flag it: to them
|
|
82
|
+
# the symbol exists. Ruff is the one checker that does.
|
|
83
|
+
extend-select = ["TC004"]
|
|
84
|
+
|
|
78
85
|
[tool.pyright]
|
|
79
86
|
include = ["src"]
|
|
80
87
|
typeCheckingMode = "strict"
|
|
@@ -103,6 +103,12 @@ from sofias_sdk_lite.llm import (
|
|
|
103
103
|
ToolCall,
|
|
104
104
|
ToolSpec,
|
|
105
105
|
)
|
|
106
|
+
from sofias_sdk_lite.files import (
|
|
107
|
+
Attachment,
|
|
108
|
+
FileAccess,
|
|
109
|
+
FileAccessError,
|
|
110
|
+
ReturnedFile,
|
|
111
|
+
)
|
|
106
112
|
from sofias_sdk_lite.messaging import (
|
|
107
113
|
AgentTaskMessage,
|
|
108
114
|
ApiTaskMessage,
|
|
@@ -171,6 +177,11 @@ except PackageNotFoundError: # pragma: no cover - source checkout without insta
|
|
|
171
177
|
|
|
172
178
|
__all__ = [
|
|
173
179
|
"__version__",
|
|
180
|
+
# Files (0.4.0)
|
|
181
|
+
"Attachment",
|
|
182
|
+
"FileAccess",
|
|
183
|
+
"FileAccessError",
|
|
184
|
+
"ReturnedFile",
|
|
174
185
|
# Agent
|
|
175
186
|
"Agent",
|
|
176
187
|
"AgentBuilder",
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Files for third-party agents: the turn's attachments and the files you return.
|
|
2
|
+
|
|
3
|
+
Everything goes through the platform's intermediary with the turn's short-lived
|
|
4
|
+
grant (the same one your connectors use). Your agent never sees storage keys or
|
|
5
|
+
permanent links, and the grant expires when the turn does.
|
|
6
|
+
|
|
7
|
+
The task message carries what you need::
|
|
8
|
+
|
|
9
|
+
def prepare_input(self, task, history, role):
|
|
10
|
+
for attachment in task.attachment_details: # what the user attached
|
|
11
|
+
data = task.files.download(attachment) # bytes
|
|
12
|
+
saved = task.files.upload("report.csv", csv_bytes, "text/csv")
|
|
13
|
+
answer = f"Here is the report.\\n\\n{saved.artifact()}"
|
|
14
|
+
|
|
15
|
+
``task.files`` is ``None`` when the platform has no intermediary for your agent
|
|
16
|
+
(for instance, a local run): check it before using it.
|
|
17
|
+
|
|
18
|
+
Images for a vision model::
|
|
19
|
+
|
|
20
|
+
part = task.files.image_part(attachment) # OpenAI-compatible content part
|
|
21
|
+
messages = [{"role": "user", "content": [{"type": "text", "text": "What is it?"}, part]}]
|
|
22
|
+
|
|
23
|
+
Limits: 25 MB per file and 10 returned files per turn.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import base64
|
|
29
|
+
import json
|
|
30
|
+
from typing import Any, ClassVar
|
|
31
|
+
from urllib.parse import quote
|
|
32
|
+
|
|
33
|
+
import httpx
|
|
34
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"MAX_FILE_BYTES",
|
|
38
|
+
"Attachment",
|
|
39
|
+
"FileAccess",
|
|
40
|
+
"FileAccessError",
|
|
41
|
+
"ReturnedFile",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
#: The intermediary rejects bigger files; checked here too, before sending.
|
|
45
|
+
MAX_FILE_BYTES = 25 * 1024 * 1024
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FileAccessError(RuntimeError):
|
|
49
|
+
"""A download or upload the intermediary refused (status in ``status_code``)."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
52
|
+
super().__init__(message)
|
|
53
|
+
self.status_code = status_code
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Attachment(BaseModel):
|
|
57
|
+
"""A file the user attached, as listed in ``task.attachment_details``."""
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(extra="ignore")
|
|
60
|
+
|
|
61
|
+
id: str | None = None
|
|
62
|
+
filename: str
|
|
63
|
+
content_type: str | None = None
|
|
64
|
+
byte_size: int | None = None
|
|
65
|
+
url: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ReturnedFile(BaseModel):
|
|
69
|
+
"""A file you uploaded with ``FileAccess.upload``: return it with ``artifact()``."""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(extra="ignore")
|
|
72
|
+
|
|
73
|
+
id: str
|
|
74
|
+
filename: str
|
|
75
|
+
content_type: str | None = None
|
|
76
|
+
byte_size: int | None = None
|
|
77
|
+
url: str | None = None
|
|
78
|
+
|
|
79
|
+
def artifact(self) -> str:
|
|
80
|
+
"""The tag that hands this file to the user. Put it in your answer."""
|
|
81
|
+
return "[ARTIFACT]\n" + json.dumps({"file_id": self.id}) + "\n[/ARTIFACT]"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class FileAccess(BaseModel):
|
|
85
|
+
"""Download the turn's attachments and upload the files you return.
|
|
86
|
+
|
|
87
|
+
Built by the SDK from the ``files`` entry of the task message; the token is
|
|
88
|
+
the turn's grant and never appears in ``repr`` or logs.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
model_config = ConfigDict(extra="ignore")
|
|
92
|
+
|
|
93
|
+
url: str
|
|
94
|
+
token: str = Field(repr=False)
|
|
95
|
+
timeout: float = 60.0
|
|
96
|
+
|
|
97
|
+
#: Tests swap the HTTP transport here (``httpx.MockTransport``).
|
|
98
|
+
_transport: ClassVar[httpx.BaseTransport | None] = None
|
|
99
|
+
_async_transport: ClassVar[httpx.AsyncBaseTransport | None] = None
|
|
100
|
+
|
|
101
|
+
# -- download -----------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def download(self, attachment: Attachment | str) -> bytes:
|
|
104
|
+
"""The bytes of an attachment (or of an intermediary file URL)."""
|
|
105
|
+
with httpx.Client(timeout=self.timeout, transport=self._transport) as client:
|
|
106
|
+
response = client.get(_url_of(attachment), headers=self._headers())
|
|
107
|
+
return _content(response, "download")
|
|
108
|
+
|
|
109
|
+
async def adownload(self, attachment: Attachment | str) -> bytes:
|
|
110
|
+
"""Async ``download``."""
|
|
111
|
+
async with httpx.AsyncClient(
|
|
112
|
+
timeout=self.timeout, transport=self._async_transport
|
|
113
|
+
) as client:
|
|
114
|
+
response = await client.get(_url_of(attachment), headers=self._headers())
|
|
115
|
+
return _content(response, "download")
|
|
116
|
+
|
|
117
|
+
def image_part(self, attachment: Attachment) -> dict[str, Any]:
|
|
118
|
+
"""An OpenAI-compatible ``image_url`` content part for a vision model."""
|
|
119
|
+
return _image_part(attachment, self.download(attachment))
|
|
120
|
+
|
|
121
|
+
async def aimage_part(self, attachment: Attachment) -> dict[str, Any]:
|
|
122
|
+
"""Async ``image_part``."""
|
|
123
|
+
return _image_part(attachment, await self.adownload(attachment))
|
|
124
|
+
|
|
125
|
+
# -- upload -------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def upload(
|
|
128
|
+
self, filename: str, data: bytes, content_type: str = "application/octet-stream"
|
|
129
|
+
) -> ReturnedFile:
|
|
130
|
+
"""Store a file for this turn; return it to the user with ``.artifact()``."""
|
|
131
|
+
_check_size(data)
|
|
132
|
+
with httpx.Client(timeout=self.timeout, transport=self._transport) as client:
|
|
133
|
+
response = client.post(
|
|
134
|
+
self._upload_url(filename), content=data, headers=self._headers(content_type)
|
|
135
|
+
)
|
|
136
|
+
return _returned(response)
|
|
137
|
+
|
|
138
|
+
async def aupload(
|
|
139
|
+
self, filename: str, data: bytes, content_type: str = "application/octet-stream"
|
|
140
|
+
) -> ReturnedFile:
|
|
141
|
+
"""Async ``upload``."""
|
|
142
|
+
_check_size(data)
|
|
143
|
+
async with httpx.AsyncClient(
|
|
144
|
+
timeout=self.timeout, transport=self._async_transport
|
|
145
|
+
) as client:
|
|
146
|
+
response = await client.post(
|
|
147
|
+
self._upload_url(filename), content=data, headers=self._headers(content_type)
|
|
148
|
+
)
|
|
149
|
+
return _returned(response)
|
|
150
|
+
|
|
151
|
+
# -- internals ----------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def _headers(self, content_type: str | None = None) -> dict[str, str]:
|
|
154
|
+
headers = {"Authorization": f"Bearer {self.token}"}
|
|
155
|
+
if content_type:
|
|
156
|
+
headers["Content-Type"] = content_type
|
|
157
|
+
return headers
|
|
158
|
+
|
|
159
|
+
def _upload_url(self, filename: str) -> str:
|
|
160
|
+
return f"{self.url.rstrip('/')}?filename={quote(filename)}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _url_of(attachment: Attachment | str) -> str:
|
|
164
|
+
return attachment if isinstance(attachment, str) else attachment.url
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _check_size(data: bytes) -> None:
|
|
168
|
+
if not data:
|
|
169
|
+
raise ValueError("cannot upload an empty file")
|
|
170
|
+
if len(data) > MAX_FILE_BYTES:
|
|
171
|
+
raise ValueError(f"file too large: {len(data)} bytes (limit {MAX_FILE_BYTES})")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _content(response: httpx.Response, action: str) -> bytes:
|
|
175
|
+
if response.status_code != 200:
|
|
176
|
+
raise FileAccessError(
|
|
177
|
+
f"file {action} failed (HTTP {response.status_code})", response.status_code
|
|
178
|
+
)
|
|
179
|
+
return response.content
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _returned(response: httpx.Response) -> ReturnedFile:
|
|
183
|
+
if response.status_code != 201:
|
|
184
|
+
raise FileAccessError(
|
|
185
|
+
f"file upload failed (HTTP {response.status_code})", response.status_code
|
|
186
|
+
)
|
|
187
|
+
return ReturnedFile.model_validate(response.json())
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _image_part(attachment: Attachment, data: bytes) -> dict[str, Any]:
|
|
191
|
+
content_type = attachment.content_type or "image/png"
|
|
192
|
+
if not content_type.startswith("image/"):
|
|
193
|
+
raise ValueError(f"{attachment.filename} is not an image ({content_type})")
|
|
194
|
+
encoded = base64.b64encode(data).decode("ascii")
|
|
195
|
+
return {"type": "image_url", "image_url": {"url": f"data:{content_type};base64,{encoded}"}}
|
|
@@ -37,6 +37,7 @@ from pydantic import BaseModel, Field, field_validator, model_validator
|
|
|
37
37
|
import nh3
|
|
38
38
|
|
|
39
39
|
from sofias_sdk_lite.contracts.strict_contract import StrictContract
|
|
40
|
+
from sofias_sdk_lite.files import Attachment, FileAccess
|
|
40
41
|
from sofias_sdk_lite.observability._log import get_logger
|
|
41
42
|
|
|
42
43
|
logger = get_logger("messaging.models")
|
|
@@ -105,6 +106,15 @@ class _BaseTaskMessage(BaseModel):
|
|
|
105
106
|
available_agents: dict[str, Any] | list[str] = {}
|
|
106
107
|
agent_internal_capabilities: str | None = None
|
|
107
108
|
|
|
109
|
+
# --- Files (0.4.0) ---
|
|
110
|
+
# What the user attached: download URLs, and their details. For third-party
|
|
111
|
+
# agents both point at the platform's intermediary, with the turn's grant.
|
|
112
|
+
attachments: list[str] = []
|
|
113
|
+
attachment_details: list[Attachment] = []
|
|
114
|
+
# Where to download from and upload to with the turn's grant (``None`` when
|
|
115
|
+
# the platform sends no intermediary, e.g. a local run). See ``sofias_sdk_lite.files``.
|
|
116
|
+
files: FileAccess | None = None
|
|
117
|
+
|
|
108
118
|
# --- Delegation (optional) ---
|
|
109
119
|
reply_to: str | None = None
|
|
110
120
|
correlation_id: str | None = None
|
|
@@ -48,6 +48,7 @@ from sofias_sdk_lite.llm import (
|
|
|
48
48
|
)
|
|
49
49
|
from sofias_sdk_lite.nodes.base_node import BaseNode, NodeType
|
|
50
50
|
from sofias_sdk_lite.nodes.prompt_assembler import PromptAssembler
|
|
51
|
+
from sofias_sdk_lite.observability.tool_events import emit_tool_call
|
|
51
52
|
from sofias_sdk_lite.observability._log import get_logger
|
|
52
53
|
|
|
53
54
|
if TYPE_CHECKING:
|
|
@@ -599,6 +600,7 @@ class LLMNode(BaseNode):
|
|
|
599
600
|
base_prompt: str,
|
|
600
601
|
prompt: str,
|
|
601
602
|
runtime_vars: dict[str, Any],
|
|
603
|
+
input_data: dict[str, Any],
|
|
602
604
|
) -> tuple[int, list[ToolSpec] | None, str]:
|
|
603
605
|
"""Re-read the tools when the catalog changed mid-turn.
|
|
604
606
|
|
|
@@ -615,6 +617,7 @@ class LLMNode(BaseNode):
|
|
|
615
617
|
would duplicate the schemas inside the text.
|
|
616
618
|
prompt: Current prompt.
|
|
617
619
|
runtime_vars: Template variables for this turn.
|
|
620
|
+
input_data: The node's validated input for this turn.
|
|
618
621
|
|
|
619
622
|
Returns:
|
|
620
623
|
Updated ``(revision, specs, prompt)``, or the ones received when
|
|
@@ -634,14 +637,33 @@ class LLMNode(BaseNode):
|
|
|
634
637
|
return (
|
|
635
638
|
revision,
|
|
636
639
|
new_specs,
|
|
637
|
-
self._prompt_with_registry_tools(base_prompt, new_specs, runtime_vars),
|
|
640
|
+
self._prompt_with_registry_tools(base_prompt, new_specs, runtime_vars, input_data),
|
|
638
641
|
)
|
|
639
642
|
|
|
643
|
+
def _assemble_prompt(self, runtime_vars: dict[str, Any], input_data: dict[str, Any]) -> str:
|
|
644
|
+
"""Assemble this turn's prompt with the node's assembler.
|
|
645
|
+
|
|
646
|
+
``input_data`` only reaches an assembler we built ourselves. A
|
|
647
|
+
caller-supplied one was written against ``assemble(runtime_vars=...)``
|
|
648
|
+
and would reject the extra keyword, so it keeps the signature it knows.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
runtime_vars: Template variables for this turn (settings + input).
|
|
652
|
+
input_data: The node's validated input on its own.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
The assembled prompt.
|
|
656
|
+
"""
|
|
657
|
+
if not self._owns_prompt_assembler:
|
|
658
|
+
return self._prompt_assembler.assemble(runtime_vars=runtime_vars)
|
|
659
|
+
return self._prompt_assembler.assemble(runtime_vars=runtime_vars, input_data=input_data)
|
|
660
|
+
|
|
640
661
|
def _prompt_with_registry_tools(
|
|
641
662
|
self,
|
|
642
663
|
prompt: str,
|
|
643
664
|
tool_specs: list[ToolSpec] | None,
|
|
644
665
|
runtime_vars: dict[str, Any],
|
|
666
|
+
input_data: dict[str, Any],
|
|
645
667
|
) -> str:
|
|
646
668
|
"""Re-assemble the prompt including the registry's tools.
|
|
647
669
|
|
|
@@ -655,6 +677,7 @@ class LLMNode(BaseNode):
|
|
|
655
677
|
prompt: Prompt already assembled with the node's assembler.
|
|
656
678
|
tool_specs: Static plus registry tools, or None.
|
|
657
679
|
runtime_vars: Template variables for this turn.
|
|
680
|
+
input_data: The node's validated input for this turn.
|
|
658
681
|
|
|
659
682
|
Returns:
|
|
660
683
|
The re-assembled prompt, or the one received when no rebuild applies.
|
|
@@ -670,7 +693,7 @@ class LLMNode(BaseNode):
|
|
|
670
693
|
output_contract=self._config.output_contract,
|
|
671
694
|
raw_text=self._raw_text_field is not None,
|
|
672
695
|
)
|
|
673
|
-
return assembler.assemble(runtime_vars=runtime_vars)
|
|
696
|
+
return assembler.assemble(runtime_vars=runtime_vars, input_data=input_data)
|
|
674
697
|
|
|
675
698
|
async def _execute_registry_tool(self, tool_call: ToolCall, tool_start: float) -> ToolResult:
|
|
676
699
|
"""Execute a tool call via the ToolRegistry.
|
|
@@ -727,7 +750,11 @@ class LLMNode(BaseNode):
|
|
|
727
750
|
)
|
|
728
751
|
|
|
729
752
|
async def _execute_tool(self, tool_call: ToolCall) -> ToolResult:
|
|
730
|
-
"""Execute a single tool call.
|
|
753
|
+
"""Execute a single tool call and leave its event line (``emit_tool_call``).
|
|
754
|
+
|
|
755
|
+
Both paths -- streaming and not -- and both kinds of tool -- your own
|
|
756
|
+
and the connectors' -- come through here, so this is the one place the
|
|
757
|
+
sandbox logs learn which tools ran.
|
|
731
758
|
|
|
732
759
|
Args:
|
|
733
760
|
tool_call: The tool call to execute.
|
|
@@ -735,6 +762,20 @@ class LLMNode(BaseNode):
|
|
|
735
762
|
Returns:
|
|
736
763
|
ToolResult with success/failure and output/error.
|
|
737
764
|
"""
|
|
765
|
+
started = time.perf_counter()
|
|
766
|
+
result = await self._run_tool(tool_call)
|
|
767
|
+
known = tool_call.name in self._tools_by_name or self._tool_registry is not None
|
|
768
|
+
emit_tool_call(
|
|
769
|
+
tool_call.name,
|
|
770
|
+
ok=result.success,
|
|
771
|
+
duration_ms=(time.perf_counter() - started) * 1000,
|
|
772
|
+
node_name=self.name,
|
|
773
|
+
error_type=None if result.success else ("failed" if known else "not_found"),
|
|
774
|
+
)
|
|
775
|
+
return result
|
|
776
|
+
|
|
777
|
+
async def _run_tool(self, tool_call: ToolCall) -> ToolResult:
|
|
778
|
+
"""Execute a single tool call (see ``_execute_tool``)."""
|
|
738
779
|
tool_start = time.perf_counter()
|
|
739
780
|
tool = self._tools_by_name.get(tool_call.name)
|
|
740
781
|
if tool is None:
|
|
@@ -760,6 +801,12 @@ class LLMNode(BaseNode):
|
|
|
760
801
|
if modified_input is not None:
|
|
761
802
|
tool_input = modified_input
|
|
762
803
|
|
|
804
|
+
# ToolContext is a TYPE_CHECKING-only name in this module, and this is
|
|
805
|
+
# the one place that BUILDS one instead of just annotating with it, so
|
|
806
|
+
# it needs the real import at runtime. Same local-import idiom already
|
|
807
|
+
# used for DelegationNode, FanOutStrategy and StreamingResponseWorkflow.
|
|
808
|
+
from sofias_sdk_lite.tools import ToolContext
|
|
809
|
+
|
|
763
810
|
# Execute the tool using BaseTool.execute() in a worker thread (async, non-blocking)
|
|
764
811
|
try:
|
|
765
812
|
context = ToolContext(
|
|
@@ -980,7 +1027,7 @@ class LLMNode(BaseNode):
|
|
|
980
1027
|
|
|
981
1028
|
runtime_vars = self._get_runtime_vars()
|
|
982
1029
|
runtime_vars.update(input_data)
|
|
983
|
-
prompt = self.
|
|
1030
|
+
prompt = self._assemble_prompt(runtime_vars, input_data)
|
|
984
1031
|
|
|
985
1032
|
max_iterations = self._get_max_iterations()
|
|
986
1033
|
parallel_tool_calls = self._get_parallel_tool_calls()
|
|
@@ -988,7 +1035,7 @@ class LLMNode(BaseNode):
|
|
|
988
1035
|
tool_catalog_revision = self._tool_catalog_revision()
|
|
989
1036
|
|
|
990
1037
|
base_prompt = prompt
|
|
991
|
-
prompt = self._prompt_with_registry_tools(prompt, tool_specs, runtime_vars)
|
|
1038
|
+
prompt = self._prompt_with_registry_tools(prompt, tool_specs, runtime_vars, input_data)
|
|
992
1039
|
|
|
993
1040
|
history: list[tuple[LLMResponse, list[ToolResult]]] = []
|
|
994
1041
|
iteration = 0
|
|
@@ -1000,7 +1047,7 @@ class LLMNode(BaseNode):
|
|
|
1000
1047
|
|
|
1001
1048
|
while iteration < max_iterations:
|
|
1002
1049
|
tool_catalog_revision, tool_specs, prompt = await self._refresh_tool_catalog(
|
|
1003
|
-
tool_catalog_revision, tool_specs, base_prompt, prompt, runtime_vars
|
|
1050
|
+
tool_catalog_revision, tool_specs, base_prompt, prompt, runtime_vars, input_data
|
|
1004
1051
|
)
|
|
1005
1052
|
|
|
1006
1053
|
if token_budget and history:
|
|
@@ -1474,7 +1521,7 @@ class LLMNode(BaseNode):
|
|
|
1474
1521
|
runtime_vars = self._get_runtime_vars()
|
|
1475
1522
|
# Include validated input in runtime vars for prompt templates
|
|
1476
1523
|
runtime_vars.update(input_data)
|
|
1477
|
-
prompt = self.
|
|
1524
|
+
prompt = self._assemble_prompt(runtime_vars, input_data)
|
|
1478
1525
|
|
|
1479
1526
|
# Tool loop
|
|
1480
1527
|
max_iterations = self._get_max_iterations()
|
|
@@ -1483,7 +1530,9 @@ class LLMNode(BaseNode):
|
|
|
1483
1530
|
tool_catalog_revision = self._tool_catalog_revision()
|
|
1484
1531
|
|
|
1485
1532
|
base_prompt = prompt
|
|
1486
|
-
prompt = self._prompt_with_registry_tools(
|
|
1533
|
+
prompt = self._prompt_with_registry_tools(
|
|
1534
|
+
prompt, tool_specs, runtime_vars, input_data
|
|
1535
|
+
)
|
|
1487
1536
|
|
|
1488
1537
|
history: list[tuple[LLMResponse, list[ToolResult]]] = []
|
|
1489
1538
|
iteration = 0
|
|
@@ -1506,7 +1555,12 @@ class LLMNode(BaseNode):
|
|
|
1506
1555
|
|
|
1507
1556
|
while iteration < max_iterations:
|
|
1508
1557
|
tool_catalog_revision, tool_specs, prompt = await self._refresh_tool_catalog(
|
|
1509
|
-
tool_catalog_revision,
|
|
1558
|
+
tool_catalog_revision,
|
|
1559
|
+
tool_specs,
|
|
1560
|
+
base_prompt,
|
|
1561
|
+
prompt,
|
|
1562
|
+
runtime_vars,
|
|
1563
|
+
input_data,
|
|
1510
1564
|
)
|
|
1511
1565
|
|
|
1512
1566
|
if token_budget and history:
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/prompt_assembler.py
RENAMED
|
@@ -13,6 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
from typing import TYPE_CHECKING, Any
|
|
14
14
|
|
|
15
15
|
from sofias_sdk_lite.llm import ToolSpec
|
|
16
|
+
from sofias_sdk_lite.observability._log import get_logger
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from sofias_sdk_lite.contracts import StrictContract
|
|
@@ -21,6 +22,8 @@ if TYPE_CHECKING:
|
|
|
21
22
|
# Re-export so existing importers keep working
|
|
22
23
|
__all__ = ["PromptAssembler", "ToolSpec"]
|
|
23
24
|
|
|
25
|
+
logger = get_logger("nodes.prompt_assembler")
|
|
26
|
+
|
|
24
27
|
|
|
25
28
|
class PromptAssembler:
|
|
26
29
|
"""Assembles the final prompt for LLM calls.
|
|
@@ -230,7 +233,44 @@ class PromptAssembler:
|
|
|
230
233
|
"""
|
|
231
234
|
return ""
|
|
232
235
|
|
|
233
|
-
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _keys_used_by_template(template: str, input_data: dict[str, Any]) -> list[str]:
|
|
238
|
+
"""Which input keys the template actually references as {placeholders}.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
template: The fixed prompt, before variable resolution.
|
|
242
|
+
input_data: The node's validated input.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
The referenced keys, empty when the template ignores the input.
|
|
246
|
+
"""
|
|
247
|
+
return [key for key in input_data if "{" + key + "}" in template]
|
|
248
|
+
|
|
249
|
+
def _format_input_section(self, input_data: dict[str, Any]) -> str:
|
|
250
|
+
"""Format the node's input as its own section.
|
|
251
|
+
|
|
252
|
+
The input reaches the model ONLY through {placeholders} in the fixed
|
|
253
|
+
prompt: there is no separate "user message" channel. A node configured
|
|
254
|
+
without ``NodePromptConfig`` therefore sent the model the tool catalogue
|
|
255
|
+
and nothing else, dropping the user's message in silence (TKT-066).
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
input_data: The node's validated input.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Formatted input section, or empty string when there is no input.
|
|
262
|
+
"""
|
|
263
|
+
if not input_data:
|
|
264
|
+
return ""
|
|
265
|
+
|
|
266
|
+
body = json.dumps(input_data, indent=2, ensure_ascii=False, default=str)
|
|
267
|
+
return f"## Input\n\n```json\n{body}\n```"
|
|
268
|
+
|
|
269
|
+
def assemble(
|
|
270
|
+
self,
|
|
271
|
+
runtime_vars: dict[str, Any] | None = None,
|
|
272
|
+
input_data: dict[str, Any] | None = None,
|
|
273
|
+
) -> str:
|
|
234
274
|
"""Assemble the final prompt.
|
|
235
275
|
|
|
236
276
|
Combines the fixed prompt with variable sections (tools, output format)
|
|
@@ -238,6 +278,11 @@ class PromptAssembler:
|
|
|
238
278
|
|
|
239
279
|
Args:
|
|
240
280
|
runtime_vars: Optional runtime variables for template resolution.
|
|
281
|
+
input_data: The node's validated input, when the caller can tell it
|
|
282
|
+
apart from ``runtime_vars`` (which also carries agent settings).
|
|
283
|
+
Used to render the input for a node that has no prompt template,
|
|
284
|
+
and to warn when a template ignores it. Optional, so a
|
|
285
|
+
caller-supplied assembler keeps working without it.
|
|
241
286
|
|
|
242
287
|
Returns:
|
|
243
288
|
The complete assembled prompt ready for LLM invocation.
|
|
@@ -264,5 +309,24 @@ class PromptAssembler:
|
|
|
264
309
|
if output_section:
|
|
265
310
|
sections.append(output_section)
|
|
266
311
|
|
|
312
|
+
if input_data:
|
|
313
|
+
if not fixed_prompt:
|
|
314
|
+
# No template at all: without this the input never reaches the
|
|
315
|
+
# model. Appended last, after the tool schemas, so the task sits
|
|
316
|
+
# closest to where the model answers.
|
|
317
|
+
input_section = self._format_input_section(input_data)
|
|
318
|
+
if input_section:
|
|
319
|
+
sections.append(input_section)
|
|
320
|
+
elif not self._keys_used_by_template(fixed_prompt, input_data):
|
|
321
|
+
# A template that names none of the input keys drops the input
|
|
322
|
+
# just as silently, but it may be deliberate (a node whose input
|
|
323
|
+
# only passes through to the output), so this only warns.
|
|
324
|
+
logger.warning(
|
|
325
|
+
"Prompt template references none of the node's input: the "
|
|
326
|
+
"model will not see it. Reference it as {placeholder}.",
|
|
327
|
+
node_name=self._config.name,
|
|
328
|
+
input_keys=sorted(input_data),
|
|
329
|
+
)
|
|
330
|
+
|
|
267
331
|
# Join sections with clear separation
|
|
268
332
|
return "\n\n".join(sections).strip()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""One line per tool call, for the developer portal's sandbox logs.
|
|
2
|
+
|
|
3
|
+
Every tool the model calls -- your own or a connector's -- leaves one line on
|
|
4
|
+
standard output, whatever your logging configuration::
|
|
5
|
+
|
|
6
|
+
SOFIAS_EVENT {"type":"tool_call","tool":"search","server":"notion","ok":true,"duration_ms":412,...}
|
|
7
|
+
|
|
8
|
+
The portal reads these lines to show which tools your agent called. They carry
|
|
9
|
+
names, timings and outcome only: never the arguments or the results, which may
|
|
10
|
+
hold your users' data. Set ``SOFIAS_EVENTS=off`` to silence them.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
__all__ = ["EVENT_PREFIX", "emit_tool_call"]
|
|
23
|
+
|
|
24
|
+
#: Marks the lines the portal parses; everything else in the log is shown raw.
|
|
25
|
+
EVENT_PREFIX = "SOFIAS_EVENT"
|
|
26
|
+
|
|
27
|
+
_logger = logging.getLogger("sofias_sdk_lite.events")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _StdoutHandler(logging.StreamHandler): # type: ignore[type-arg]
|
|
31
|
+
"""Writes to whatever ``sys.stdout`` is at emit time (it may be swapped later)."""
|
|
32
|
+
|
|
33
|
+
@property # type: ignore[override]
|
|
34
|
+
def stream(self) -> Any:
|
|
35
|
+
return sys.stdout
|
|
36
|
+
|
|
37
|
+
@stream.setter
|
|
38
|
+
def stream(self, _value: Any) -> None:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ensure_handler() -> None:
|
|
43
|
+
"""Own stdout handler, so the line appears even without logging configured."""
|
|
44
|
+
if _logger.handlers:
|
|
45
|
+
return
|
|
46
|
+
handler = _StdoutHandler()
|
|
47
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
48
|
+
_logger.addHandler(handler)
|
|
49
|
+
_logger.setLevel(logging.INFO)
|
|
50
|
+
_logger.propagate = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def emit_tool_call(
|
|
54
|
+
tool_name: str,
|
|
55
|
+
*,
|
|
56
|
+
ok: bool,
|
|
57
|
+
duration_ms: float,
|
|
58
|
+
node_name: str | None = None,
|
|
59
|
+
error_type: str | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Write the event line for one tool call (no arguments, no results)."""
|
|
62
|
+
if os.environ.get("SOFIAS_EVENTS", "").lower() in {"off", "0", "false"}:
|
|
63
|
+
return
|
|
64
|
+
server, separator, tool = tool_name.partition("__")
|
|
65
|
+
if not separator:
|
|
66
|
+
server, tool = "", tool_name
|
|
67
|
+
event: dict[str, Any] = {
|
|
68
|
+
"type": "tool_call",
|
|
69
|
+
"tool": tool,
|
|
70
|
+
"server": server or None,
|
|
71
|
+
"ok": ok,
|
|
72
|
+
"duration_ms": round(duration_ms),
|
|
73
|
+
"node": node_name,
|
|
74
|
+
"error_type": error_type,
|
|
75
|
+
"ts": round(time.time(), 3),
|
|
76
|
+
}
|
|
77
|
+
_ensure_handler()
|
|
78
|
+
_logger.info(
|
|
79
|
+
"%s %s", EVENT_PREFIX, json.dumps(event, separators=(",", ":"), ensure_ascii=False)
|
|
80
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/execution_context.py
RENAMED
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/agent/response_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/agent_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/config/runtime_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/agent_contracts.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/base_contracts.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/node_contracts.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/contracts/strict_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/errors/circuit_breaker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/llm/openai_compatible.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/aggregator_config.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/aggregator_node.py
RENAMED
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/delegation_config.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/delegation_node.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/delegation_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/llm_node_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/nodes/planning_models.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/trace_context.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/observability/tracing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/rabbitmq/delegation_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/state/conversation_state.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.1}/src/sofias_sdk_lite/workflows/streaming_chat.py
RENAMED
|
File without changes
|