sofias-sdk-lite 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/PKG-INFO +1 -1
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/pyproject.toml +1 -1
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/pyproject.toml.orig +1 -1
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/__init__.py +11 -0
- sofias_sdk_lite-0.4.0/src/sofias_sdk_lite/files.py +195 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/messaging/models.py +10 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/llm_node.py +20 -1
- sofias_sdk_lite-0.4.0/src/sofias_sdk_lite/observability/tool_events.py +80 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/LICENSE +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/README.md +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/agent/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/agent/agent.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/agent/agent_builder.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/agent/agent_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/agent/execution_context.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/agent/middleware.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/agent/response_workflow.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/agent/streaming.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/config/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/config/agent_settings.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/config/defaults.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/config/runtime_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/config/sdk_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/config/source.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/contracts/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/contracts/agent_contracts.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/contracts/base_contracts.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/contracts/node_contracts.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/contracts/strict_contract.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/errors/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/errors/circuit_breaker.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/errors/error_handler.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/errors/exceptions.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/llm/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/llm/factory.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/llm/gateway.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/llm/openai_compatible.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/llm/protocol.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/llm/tokens.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/llm/tools.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/mcp.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/messaging/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/aggregator_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/aggregator_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/base_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/delegation_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/delegation_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/delegation_transport.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/function_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/llm_node_config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/plan_executor.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/planner_node.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/planning_models.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/prompt_assembler.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/observability/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/observability/_log.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/observability/events.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/observability/trace_context.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/observability/tracing.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/py.typed +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/rabbitmq/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/rabbitmq/client.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/rabbitmq/config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/rabbitmq/consumer.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/rabbitmq/delegation_transport.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/rabbitmq/publisher.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/rabbitmq/rpc_client.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/rabbitmq/types.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/routing/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/routing/router.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/routing/strategies.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/runner/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/runner/config.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/runner/runner.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/state/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/state/conversation_state.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/state/history.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/state/memory.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/tools.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/workflows/__init__.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/workflows/chat.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/workflows/null.py +0 -0
- {sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/workflows/streaming_chat.py +0 -0
|
@@ -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:
|
|
@@ -727,7 +728,11 @@ class LLMNode(BaseNode):
|
|
|
727
728
|
)
|
|
728
729
|
|
|
729
730
|
async def _execute_tool(self, tool_call: ToolCall) -> ToolResult:
|
|
730
|
-
"""Execute a single tool call.
|
|
731
|
+
"""Execute a single tool call and leave its event line (``emit_tool_call``).
|
|
732
|
+
|
|
733
|
+
Both paths -- streaming and not -- and both kinds of tool -- your own
|
|
734
|
+
and the connectors' -- come through here, so this is the one place the
|
|
735
|
+
sandbox logs learn which tools ran.
|
|
731
736
|
|
|
732
737
|
Args:
|
|
733
738
|
tool_call: The tool call to execute.
|
|
@@ -735,6 +740,20 @@ class LLMNode(BaseNode):
|
|
|
735
740
|
Returns:
|
|
736
741
|
ToolResult with success/failure and output/error.
|
|
737
742
|
"""
|
|
743
|
+
started = time.perf_counter()
|
|
744
|
+
result = await self._run_tool(tool_call)
|
|
745
|
+
known = tool_call.name in self._tools_by_name or self._tool_registry is not None
|
|
746
|
+
emit_tool_call(
|
|
747
|
+
tool_call.name,
|
|
748
|
+
ok=result.success,
|
|
749
|
+
duration_ms=(time.perf_counter() - started) * 1000,
|
|
750
|
+
node_name=self.name,
|
|
751
|
+
error_type=None if result.success else ("failed" if known else "not_found"),
|
|
752
|
+
)
|
|
753
|
+
return result
|
|
754
|
+
|
|
755
|
+
async def _run_tool(self, tool_call: ToolCall) -> ToolResult:
|
|
756
|
+
"""Execute a single tool call (see ``_execute_tool``)."""
|
|
738
757
|
tool_start = time.perf_counter()
|
|
739
758
|
tool = self._tools_by_name.get(tool_call.name)
|
|
740
759
|
if tool is None:
|
|
@@ -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.0}/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.0}/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.0}/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.0}/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.0}/src/sofias_sdk_lite/contracts/agent_contracts.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/contracts/base_contracts.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/contracts/node_contracts.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/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.0}/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.0}/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.0}/src/sofias_sdk_lite/nodes/aggregator_config.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/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.0}/src/sofias_sdk_lite/nodes/delegation_config.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/delegation_node.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/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.0}/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.0}/src/sofias_sdk_lite/nodes/planning_models.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/src/sofias_sdk_lite/nodes/prompt_assembler.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/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.0}/src/sofias_sdk_lite/observability/trace_context.py
RENAMED
|
File without changes
|
{sofias_sdk_lite-0.3.0 → sofias_sdk_lite-0.4.0}/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.0}/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.0}/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.0}/src/sofias_sdk_lite/workflows/streaming_chat.py
RENAMED
|
File without changes
|