trytilde 3.0.1__py3-none-any.whl
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.
- tilde/__init__.py +114 -0
- tilde/__main__.py +9 -0
- tilde/_cancel.py +50 -0
- tilde/_inference.py +106 -0
- tilde/_invocation.py +26 -0
- tilde/_otlp.py +35 -0
- tilde/_tools.py +182 -0
- tilde/_transport.py +28 -0
- tilde/agent_event_ingress/__init__.py +0 -0
- tilde/agent_event_ingress/v1/__init__.py +0 -0
- tilde/agent_event_ingress/v1/sidecars_connect.py +748 -0
- tilde/agent_event_ingress/v1/sidecars_pb2.py +128 -0
- tilde/agent_event_ingress/v1/sidecars_pb2.pyi +471 -0
- tilde/agent_host/__init__.py +0 -0
- tilde/agent_host/v1/__init__.py +0 -0
- tilde/agent_host/v1/agent_pb2.py +43 -0
- tilde/agent_host/v1/agent_pb2.pyi +56 -0
- tilde/channels.py +212 -0
- tilde/chat_proxy.py +295 -0
- tilde/clients.py +127 -0
- tilde/context.py +1006 -0
- tilde/deploy.py +420 -0
- tilde/discovery.py +127 -0
- tilde/fastapi.py +69 -0
- tilde/host.py +481 -0
- tilde/logging.py +213 -0
- tilde/management/__init__.py +0 -0
- tilde/management/v1/__init__.py +0 -0
- tilde/management/v1/access_connect.py +465 -0
- tilde/management/v1/access_pb2.py +57 -0
- tilde/management/v1/access_pb2.pyi +102 -0
- tilde/management/v1/agents_connect.py +668 -0
- tilde/management/v1/agents_pb2.py +73 -0
- tilde/management/v1/agents_pb2.pyi +132 -0
- tilde/management/v1/connections_connect.py +806 -0
- tilde/management/v1/connections_pb2.py +93 -0
- tilde/management/v1/connections_pb2.pyi +186 -0
- tilde/management/v1/deployments_connect.py +919 -0
- tilde/management/v1/deployments_pb2.py +109 -0
- tilde/management/v1/deployments_pb2.pyi +306 -0
- tilde/management/v1/identities_connect.py +660 -0
- tilde/management/v1/identities_pb2.py +74 -0
- tilde/management/v1/identities_pb2.pyi +167 -0
- tilde/management/v1/inference_connect.py +424 -0
- tilde/management/v1/inference_pb2.py +69 -0
- tilde/management/v1/inference_pb2.pyi +158 -0
- tilde/management/v1/logs_connect.py +278 -0
- tilde/management/v1/logs_pb2.py +56 -0
- tilde/management/v1/logs_pb2.pyi +123 -0
- tilde/management/v1/prompts_connect.py +290 -0
- tilde/management/v1/prompts_pb2.py +49 -0
- tilde/management/v1/prompts_pb2.pyi +36 -0
- tilde/management/v1/skills_connect.py +1815 -0
- tilde/management/v1/skills_pb2.py +147 -0
- tilde/management/v1/skills_pb2.pyi +324 -0
- tilde/management/v1/tilde_chat_connect.py +290 -0
- tilde/management/v1/tilde_chat_pb2.py +48 -0
- tilde/management/v1/tilde_chat_pb2.pyi +29 -0
- tilde/management/v1/tools_connect.py +1416 -0
- tilde/management/v1/tools_pb2.py +135 -0
- tilde/management/v1/tools_pb2.pyi +341 -0
- tilde/management/v1/tracing_connect.py +513 -0
- tilde/management/v1/tracing_pb2.py +78 -0
- tilde/management/v1/tracing_pb2.pyi +231 -0
- tilde/messages.py +129 -0
- tilde/prompts.py +130 -0
- tilde/provider/__init__.py +0 -0
- tilde/provider/tilde/__init__.py +0 -0
- tilde/provider/tilde/v1/__init__.py +0 -0
- tilde/provider/tilde/v1/chat_connect.py +2090 -0
- tilde/provider/tilde/v1/chat_pb2.py +165 -0
- tilde/provider/tilde/v1/chat_pb2.pyi +478 -0
- tilde/provider/v1/__init__.py +0 -0
- tilde/provider/v1/connections_connect.py +205 -0
- tilde/provider/v1/connections_pb2.py +49 -0
- tilde/provider/v1/connections_pb2.pyi +58 -0
- tilde/run/__init__.py +0 -0
- tilde/run/v1/__init__.py +0 -0
- tilde/run/v1/run_connect.py +367 -0
- tilde/run/v1/run_pb2.py +57 -0
- tilde/run/v1/run_pb2.pyi +80 -0
- tilde/runtime/__init__.py +0 -0
- tilde/runtime/v1/__init__.py +0 -0
- tilde/runtime/v1/agents_connect.py +473 -0
- tilde/runtime/v1/agents_pb2.py +61 -0
- tilde/runtime/v1/agents_pb2.pyi +86 -0
- tilde/runtime/v1/cache_pb2.py +38 -0
- tilde/runtime/v1/cache_pb2.pyi +13 -0
- tilde/runtime/v1/chat_connect.py +1460 -0
- tilde/runtime/v1/chat_pb2.py +130 -0
- tilde/runtime/v1/chat_pb2.pyi +275 -0
- tilde/runtime/v1/controls_connect.py +282 -0
- tilde/runtime/v1/controls_pb2.py +47 -0
- tilde/runtime/v1/controls_pb2.pyi +49 -0
- tilde/runtime/v1/prompts_connect.py +225 -0
- tilde/runtime/v1/prompts_pb2.py +43 -0
- tilde/runtime/v1/prompts_pb2.pyi +20 -0
- tilde/runtime/v1/skills_connect.py +655 -0
- tilde/runtime/v1/skills_pb2.py +77 -0
- tilde/runtime/v1/skills_pb2.pyi +154 -0
- tilde/setup/__init__.py +0 -0
- tilde/setup/v1/__init__.py +0 -0
- tilde/setup/v1/connections_connect.py +595 -0
- tilde/setup/v1/connections_pb2.py +101 -0
- tilde/setup/v1/connections_pb2.pyi +232 -0
- tilde/setup/v1/identity_verification_connect.py +282 -0
- tilde/setup/v1/identity_verification_pb2.py +49 -0
- tilde/setup/v1/identity_verification_pb2.pyi +35 -0
- tilde/skills.py +383 -0
- tilde/tool_host/v1/tool_host_connect.py +314 -0
- tilde/tool_host/v1/tool_host_pb2.py +66 -0
- tilde/tool_host/v1/tool_host_pb2.pyi +115 -0
- tilde/tool_hosts.py +515 -0
- tilde/tracing.py +147 -0
- tilde/types/__init__.py +0 -0
- tilde/types/v1/__init__.py +0 -0
- tilde/types/v1/access_pb2.py +47 -0
- tilde/types/v1/access_pb2.pyi +123 -0
- tilde/types/v1/agent_pb2.py +53 -0
- tilde/types/v1/agent_pb2.pyi +151 -0
- tilde/types/v1/chat_pb2.py +81 -0
- tilde/types/v1/chat_pb2.pyi +316 -0
- tilde/types/v1/connections_pb2.py +79 -0
- tilde/types/v1/connections_pb2.pyi +268 -0
- tilde/types/v1/deployment_pb2.py +51 -0
- tilde/types/v1/deployment_pb2.pyi +129 -0
- tilde/types/v1/prompt_pb2.py +45 -0
- tilde/types/v1/prompt_pb2.pyi +96 -0
- tilde/types/v1/runtime_event_pb2.py +62 -0
- tilde/types/v1/runtime_event_pb2.pyi +243 -0
- tilde/types/v1/skill_pb2.py +49 -0
- tilde/types/v1/skill_pb2.pyi +150 -0
- trytilde-3.0.1.dist-info/METADATA +325 -0
- trytilde-3.0.1.dist-info/RECORD +135 -0
- trytilde-3.0.1.dist-info/WHEEL +4 -0
tilde/__init__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Tilde Python SDK: agent hosts, invocation context, channel tools and management clients.
|
|
2
|
+
|
|
3
|
+
Generated contracts live beside these modules under the proto package names, for example
|
|
4
|
+
``tilde.runtime.v1.chat_pb2`` and ``tilde.types.v1.chat_pb2``. Run ``sdk/py/scripts/generate.py``
|
|
5
|
+
after changing proto/.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from tilde._cancel import Cancellation, InvocationCancelled, StopLoop
|
|
9
|
+
from tilde._inference import Inference, inference
|
|
10
|
+
from tilde._invocation import current_invocation
|
|
11
|
+
from tilde._tools import (
|
|
12
|
+
BundledOptions,
|
|
13
|
+
BundledTools,
|
|
14
|
+
Tool,
|
|
15
|
+
ToolAnnotations,
|
|
16
|
+
ToolCatalog,
|
|
17
|
+
define_tools,
|
|
18
|
+
tool_id,
|
|
19
|
+
)
|
|
20
|
+
from tilde.channels import Channels, ChannelTool, ChannelTools
|
|
21
|
+
from tilde.clients import (
|
|
22
|
+
ManagementClient,
|
|
23
|
+
RuntimeClient,
|
|
24
|
+
create_management_client,
|
|
25
|
+
create_runtime_client,
|
|
26
|
+
create_tilde_chat_client,
|
|
27
|
+
)
|
|
28
|
+
from tilde.context import AgentContext, DownloadedAttachment, SteeringInput
|
|
29
|
+
from tilde.host import (
|
|
30
|
+
ConnectedAgent,
|
|
31
|
+
HostState,
|
|
32
|
+
InvocationOptions,
|
|
33
|
+
Wake,
|
|
34
|
+
connect_agent,
|
|
35
|
+
create_lambda_handler,
|
|
36
|
+
run_connected_agent,
|
|
37
|
+
run_invocation,
|
|
38
|
+
)
|
|
39
|
+
from tilde.logging import DeploymentLogging, agent_log_processor, configure_deployment_logging
|
|
40
|
+
from tilde.messages import (
|
|
41
|
+
ContextMessage,
|
|
42
|
+
ConversationMessage,
|
|
43
|
+
GoalMessage,
|
|
44
|
+
MessageClient,
|
|
45
|
+
MessageHistory,
|
|
46
|
+
ObjectiveMessage,
|
|
47
|
+
TaskMessage,
|
|
48
|
+
)
|
|
49
|
+
from tilde.prompts import PromptDefinition, define_prompt
|
|
50
|
+
from tilde.skills import (
|
|
51
|
+
SkillDefinition,
|
|
52
|
+
SkillFile,
|
|
53
|
+
SkillFileBody,
|
|
54
|
+
SkillsClient,
|
|
55
|
+
SkillsDefinition,
|
|
56
|
+
define_skill,
|
|
57
|
+
define_skills,
|
|
58
|
+
)
|
|
59
|
+
from tilde.tracing import agent_span_processor
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"Inference",
|
|
63
|
+
"AgentContext",
|
|
64
|
+
"BundledOptions",
|
|
65
|
+
"BundledTools",
|
|
66
|
+
"Cancellation",
|
|
67
|
+
"ChannelTool",
|
|
68
|
+
"ChannelTools",
|
|
69
|
+
"Channels",
|
|
70
|
+
"ConnectedAgent",
|
|
71
|
+
"ContextMessage",
|
|
72
|
+
"ConversationMessage",
|
|
73
|
+
"DeploymentLogging",
|
|
74
|
+
"DownloadedAttachment",
|
|
75
|
+
"GoalMessage",
|
|
76
|
+
"HostState",
|
|
77
|
+
"InvocationCancelled",
|
|
78
|
+
"InvocationOptions",
|
|
79
|
+
"ManagementClient",
|
|
80
|
+
"MessageClient",
|
|
81
|
+
"MessageHistory",
|
|
82
|
+
"ObjectiveMessage",
|
|
83
|
+
"PromptDefinition",
|
|
84
|
+
"RuntimeClient",
|
|
85
|
+
"SkillDefinition",
|
|
86
|
+
"SkillFile",
|
|
87
|
+
"SkillFileBody",
|
|
88
|
+
"SkillsClient",
|
|
89
|
+
"SkillsDefinition",
|
|
90
|
+
"SteeringInput",
|
|
91
|
+
"StopLoop",
|
|
92
|
+
"TaskMessage",
|
|
93
|
+
"Tool",
|
|
94
|
+
"ToolAnnotations",
|
|
95
|
+
"ToolCatalog",
|
|
96
|
+
"Wake",
|
|
97
|
+
"agent_log_processor",
|
|
98
|
+
"agent_span_processor",
|
|
99
|
+
"configure_deployment_logging",
|
|
100
|
+
"connect_agent",
|
|
101
|
+
"create_lambda_handler",
|
|
102
|
+
"create_management_client",
|
|
103
|
+
"create_runtime_client",
|
|
104
|
+
"create_tilde_chat_client",
|
|
105
|
+
"current_invocation",
|
|
106
|
+
"define_prompt",
|
|
107
|
+
"define_skill",
|
|
108
|
+
"define_skills",
|
|
109
|
+
"define_tools",
|
|
110
|
+
"inference",
|
|
111
|
+
"run_connected_agent",
|
|
112
|
+
"run_invocation",
|
|
113
|
+
"tool_id",
|
|
114
|
+
]
|
tilde/__main__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""``python -m tilde deploy [ENTRY] ...``; see ``tilde.deploy``."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from tilde.deploy import main
|
|
6
|
+
|
|
7
|
+
if len(sys.argv) < 2 or sys.argv[1] != "deploy":
|
|
8
|
+
sys.exit("usage: python -m tilde deploy [ENTRY] [options] (see python -m tilde deploy --help)")
|
|
9
|
+
sys.exit(main(sys.argv[2:]))
|
tilde/_cancel.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Cooperative cancellation shared by the host and the invocation context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StopLoop(BaseException):
|
|
10
|
+
"""Raised by ``ctx.stop()``; frameworks must let it propagate like a cancellation."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InvocationCancelled(BaseException):
|
|
14
|
+
"""The invocation is no longer active (stop control, host shutdown, lost callbacks)."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Cancellation:
|
|
18
|
+
"""AbortSignal equivalent: aborts once, remembers why and notifies registered callbacks."""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._event = asyncio.Event()
|
|
22
|
+
self.reason: BaseException | None = None
|
|
23
|
+
self._callbacks: list[Callable[[], None]] = []
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def aborted(self) -> bool:
|
|
27
|
+
return self._event.is_set()
|
|
28
|
+
|
|
29
|
+
def abort(self, reason: BaseException | None = None) -> None:
|
|
30
|
+
if self._event.is_set():
|
|
31
|
+
return
|
|
32
|
+
self.reason = reason
|
|
33
|
+
self._event.set()
|
|
34
|
+
callbacks, self._callbacks = self._callbacks, []
|
|
35
|
+
for callback in callbacks:
|
|
36
|
+
callback()
|
|
37
|
+
|
|
38
|
+
def on_abort(self, callback: Callable[[], None]) -> None:
|
|
39
|
+
if self._event.is_set():
|
|
40
|
+
callback()
|
|
41
|
+
else:
|
|
42
|
+
self._callbacks.append(callback)
|
|
43
|
+
|
|
44
|
+
def check(self) -> None:
|
|
45
|
+
"""Raise when aborted, like ``AbortSignal.throwIfAborted``."""
|
|
46
|
+
if self._event.is_set():
|
|
47
|
+
raise self.reason if self.reason is not None else InvocationCancelled()
|
|
48
|
+
|
|
49
|
+
async def wait(self) -> None:
|
|
50
|
+
await self._event.wait()
|
tilde/_inference.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Route model calls through Tilde's inference gateway with the invocation's own credentials.
|
|
2
|
+
|
|
3
|
+
``ctx.inference("openai/prod")`` (or an alias the agent was given, such as ``"default"``)
|
|
4
|
+
returns what an OpenAI-compatible client needs: ``base_url``, a placeholder ``api_key`` and an
|
|
5
|
+
``httpx`` client whose auth hook sends the current, renewed invocation token on every request.
|
|
6
|
+
The gateway swaps in the real provider credential and accounts the call, so agents never hold
|
|
7
|
+
provider keys.
|
|
8
|
+
|
|
9
|
+
``tilde.inference(alias)`` is the module-level form for model clients built at import time:
|
|
10
|
+
every request resolves the invocation running it (token, callback URL, prompt stamps), and a
|
|
11
|
+
request made outside an invocation fails.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from collections.abc import Callable, Generator
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
from tilde._invocation import require_invocation
|
|
23
|
+
|
|
24
|
+
_SLUG = re.compile(r"^([a-z0-9_]+/)?[A-Za-z0-9._@+:-]+$")
|
|
25
|
+
# Module-level clients are built before any invocation exists; each request is rebased onto
|
|
26
|
+
# the invocation's callback URL, so this host is never dialed.
|
|
27
|
+
_UNBOUND = "http://tilde.invalid"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _InvocationAuth(httpx.Auth):
|
|
31
|
+
def __init__(self, prepare: Callable[[httpx.Request], httpx.Request]) -> None:
|
|
32
|
+
self._prepare = prepare
|
|
33
|
+
|
|
34
|
+
def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
|
|
35
|
+
yield self._prepare(request)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Inference:
|
|
39
|
+
"""Connection details for one inference slug."""
|
|
40
|
+
|
|
41
|
+
api_key = "tilde"
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
base_url: str,
|
|
46
|
+
authorization: Callable[[], str],
|
|
47
|
+
prompts: Callable[[], str] = lambda: "",
|
|
48
|
+
callback_url: Callable[[], str] | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.base_url = base_url
|
|
51
|
+
self._authorization = authorization
|
|
52
|
+
self._prompts = prompts
|
|
53
|
+
self._callback_url = callback_url
|
|
54
|
+
self.auth: httpx.Auth = _InvocationAuth(self.prepare)
|
|
55
|
+
|
|
56
|
+
def prepare(self, request: httpx.Request) -> httpx.Request:
|
|
57
|
+
"""Stamp one outgoing request, for frameworks that hook requests instead of clients."""
|
|
58
|
+
if self._callback_url is not None:
|
|
59
|
+
base = httpx.URL(self._callback_url().rstrip("/"))
|
|
60
|
+
request.url = request.url.copy_with(
|
|
61
|
+
scheme=base.scheme,
|
|
62
|
+
host=base.host,
|
|
63
|
+
port=base.port,
|
|
64
|
+
raw_path=base.raw_path.rstrip(b"/") + request.url.raw_path,
|
|
65
|
+
)
|
|
66
|
+
request.headers["host"] = request.url.netloc.decode("ascii")
|
|
67
|
+
# Replaces whatever key the provider SDK set; the gateway authenticates this token.
|
|
68
|
+
request.headers["authorization"] = self._authorization()
|
|
69
|
+
stamps = self._prompts()
|
|
70
|
+
if stamps:
|
|
71
|
+
request.headers["x-tilde-prompt"] = stamps
|
|
72
|
+
else:
|
|
73
|
+
request.headers.pop("x-tilde-prompt", None)
|
|
74
|
+
return request
|
|
75
|
+
|
|
76
|
+
def async_client(self, **options: Any) -> httpx.AsyncClient:
|
|
77
|
+
"""An ``httpx.AsyncClient`` for async provider SDKs (``AsyncOpenAI(http_client=...)``)."""
|
|
78
|
+
return httpx.AsyncClient(auth=self.auth, **{"timeout": 600.0, **options})
|
|
79
|
+
|
|
80
|
+
def client(self, **options: Any) -> httpx.Client:
|
|
81
|
+
"""A synchronous ``httpx.Client`` for provider SDKs that also call synchronously."""
|
|
82
|
+
return httpx.Client(auth=self.auth, **{"timeout": 600.0, **options})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _check(slug: str) -> None:
|
|
86
|
+
if not _SLUG.match(slug):
|
|
87
|
+
raise ValueError("Inference slug must be provider/account or an alias")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def bound_inference(
|
|
91
|
+
callback_url: str, slug: str, authorization: Callable[[], str], prompts: Callable[[], str]
|
|
92
|
+
) -> Inference:
|
|
93
|
+
_check(slug)
|
|
94
|
+
return Inference(f"{callback_url.rstrip('/')}/inference/{slug}", authorization, prompts)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def inference(slug: str) -> Inference:
|
|
98
|
+
"""Inference for the invocation running each request; safe to build at import time."""
|
|
99
|
+
_check(slug)
|
|
100
|
+
what = f"tilde.inference({slug!r})"
|
|
101
|
+
return Inference(
|
|
102
|
+
f"{_UNBOUND}/inference/{slug}",
|
|
103
|
+
lambda: require_invocation(what).trace_authorization(),
|
|
104
|
+
lambda: require_invocation(what).prompt_stamps(),
|
|
105
|
+
lambda: require_invocation(what).callback_url,
|
|
106
|
+
)
|
tilde/_invocation.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""The invocation the current task is running, for module-level objects built at import time."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from tilde.context import AgentContext
|
|
10
|
+
|
|
11
|
+
# Set by the host around ``run(ctx)``; asyncio tasks and ``asyncio.to_thread`` inherit it.
|
|
12
|
+
current: ContextVar[AgentContext | None] = ContextVar("tilde_invocation", default=None)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def current_invocation() -> AgentContext | None:
|
|
16
|
+
"""The running invocation's context, or ``None`` outside ``run(ctx)``."""
|
|
17
|
+
return current.get()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def require_invocation(what: str) -> AgentContext:
|
|
21
|
+
ctx = current.get()
|
|
22
|
+
if ctx is None:
|
|
23
|
+
raise RuntimeError(
|
|
24
|
+
f"{what} was used outside a Tilde invocation; call it from within run(ctx)"
|
|
25
|
+
)
|
|
26
|
+
return ctx
|
tilde/_otlp.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Minimal OTLP/HTTP protobuf uploads to the invocation callback with the current connect token."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger("tilde")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def otlp_url(base_url: str, signal: str) -> str:
|
|
15
|
+
"""Match the runtime RPC base path when a reverse proxy separates audiences by prefix."""
|
|
16
|
+
parts = urlsplit(base_url)
|
|
17
|
+
path = f"{parts.path.rstrip('/')}/v1/{signal}"
|
|
18
|
+
return urlunsplit((parts.scheme, parts.netloc, path, "", ""))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def post_protobuf(
|
|
22
|
+
url: str, payload: bytes, authorization: Callable[[], str], timeout: float = 15.0
|
|
23
|
+
) -> bool:
|
|
24
|
+
request = urllib.request.Request(
|
|
25
|
+
url,
|
|
26
|
+
data=payload,
|
|
27
|
+
method="POST",
|
|
28
|
+
headers={"Content-Type": "application/x-protobuf", "Authorization": authorization()},
|
|
29
|
+
)
|
|
30
|
+
try:
|
|
31
|
+
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 - caller-supplied callback URL
|
|
32
|
+
return 200 <= response.status < 300
|
|
33
|
+
except (urllib.error.URLError, OSError, ValueError) as error:
|
|
34
|
+
log.debug("OTLP upload to %s failed: %s", url, error)
|
|
35
|
+
return False
|
tilde/_tools.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Tools in ``ctx.tools``: provider tools published for the invocation (descriptions and schemas
|
|
2
|
+
come from Tilde) and SDK lifecycle helpers. The agent's bundled tools are its own framework tools,
|
|
3
|
+
published by each adapter's ``with_tilde_tools``."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import re
|
|
9
|
+
import uuid
|
|
10
|
+
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
ExecuteFn = Callable[[Any, str], Awaitable[Any]]
|
|
15
|
+
#: How calls of one of the agent's own tools show in end-user chats; traces keep full detail.
|
|
16
|
+
ToolDisplay = Literal["full", "summary", "hidden"]
|
|
17
|
+
StreamFn = Callable[[Any, AsyncIterable[Any], str], Awaitable[Any]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True, frozen=True)
|
|
21
|
+
class ToolAnnotations:
|
|
22
|
+
"""Hints for ordering, confirming or parallelising calls; never authorization."""
|
|
23
|
+
|
|
24
|
+
read_only: bool = False
|
|
25
|
+
destructive: bool = False
|
|
26
|
+
idempotent: bool = False
|
|
27
|
+
open_world: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class Tool:
|
|
32
|
+
"""One tool in ``ctx.tools``, or the published definition of a bundled tool."""
|
|
33
|
+
|
|
34
|
+
description: str
|
|
35
|
+
input_schema: dict[str, Any]
|
|
36
|
+
_execute: ExecuteFn = field(repr=False)
|
|
37
|
+
provider_id: str | None = None
|
|
38
|
+
chunk_schema: dict[str, Any] | None = None
|
|
39
|
+
_stream: StreamFn | None = field(default=None, repr=False)
|
|
40
|
+
output_schema: dict[str, Any] | None = None
|
|
41
|
+
#: Short label for transcripts.
|
|
42
|
+
summary: str | None = None
|
|
43
|
+
annotations: ToolAnnotations | None = None
|
|
44
|
+
#: Returns a ticket at once; the outcome arrives later as new input and through tools.result.
|
|
45
|
+
background: bool = False
|
|
46
|
+
display: ToolDisplay = "full"
|
|
47
|
+
|
|
48
|
+
async def execute(self, input: Any, *, tool_call_id: str | None = None) -> Any:
|
|
49
|
+
"""Send completed input; ``tool_call_id`` is the framework's call ID for auditing."""
|
|
50
|
+
return await self._execute(input, tool_call_id or str(uuid.uuid4()))
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def can_stream(self) -> bool:
|
|
54
|
+
return self._stream is not None
|
|
55
|
+
|
|
56
|
+
async def stream(self, input: Any, chunks: AsyncIterable[Any], *, tool_call_id: str) -> Any:
|
|
57
|
+
"""Send incremental input in the provider's ``chunk_schema`` format.
|
|
58
|
+
|
|
59
|
+
A framework must feed chunks explicitly; registering a tool never streams model tokens.
|
|
60
|
+
"""
|
|
61
|
+
if self._stream is None:
|
|
62
|
+
raise ValueError("This tool does not accept incremental input")
|
|
63
|
+
return await self._stream(input, chunks, tool_call_id)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
ToolCatalog = dict[str, Tool]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(slots=True, frozen=True)
|
|
70
|
+
class BundledOptions:
|
|
71
|
+
"""Tilde metadata for one of the agent's own framework tools, overriding what the framework
|
|
72
|
+
declares. ``display`` is how its calls show in end-user chats: with input and output, only
|
|
73
|
+
the summary, or hidden; traces keep full detail."""
|
|
74
|
+
|
|
75
|
+
summary: str | None = None
|
|
76
|
+
display: ToolDisplay = "full"
|
|
77
|
+
annotations: ToolAnnotations | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(slots=True, frozen=True)
|
|
81
|
+
class BundledTools:
|
|
82
|
+
"""An agent's own framework tools with their Tilde options, from ``define_tools``."""
|
|
83
|
+
|
|
84
|
+
tools: list[Any]
|
|
85
|
+
options: dict[str, BundledOptions]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def define_tools(
|
|
89
|
+
tools: Sequence[Any], *, options: Mapping[str, BundledOptions] | None = None
|
|
90
|
+
) -> BundledTools:
|
|
91
|
+
"""Declare the agent's bundled tools: its framework's own tools (functions, ``@tool``s,
|
|
92
|
+
``FunctionTool``s, ...) with Tilde options by tool name. Pass the result to the adapter's
|
|
93
|
+
per-invocation helper as ``bundled`` (or its parts to ``with_tilde_tools``); at module level,
|
|
94
|
+
``tilde deploy`` declares the tools with the deployment."""
|
|
95
|
+
return BundledTools(list(tools), dict(options or {}))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(slots=True, frozen=True)
|
|
99
|
+
class ToolSpec:
|
|
100
|
+
"""One of the agent's framework tools as Tilde publishes it per invocation and ``tilde deploy``
|
|
101
|
+
declares it, with its Tilde options applied. Each adapter's ``describe_tool`` builds it."""
|
|
102
|
+
|
|
103
|
+
name: str
|
|
104
|
+
description: str
|
|
105
|
+
input_schema: dict[str, Any]
|
|
106
|
+
output_schema: dict[str, Any] | None = None
|
|
107
|
+
summary: str | None = None
|
|
108
|
+
annotations: ToolAnnotations | None = None
|
|
109
|
+
display: ToolDisplay = "full"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def tool_spec(
|
|
113
|
+
name: str,
|
|
114
|
+
description: str,
|
|
115
|
+
input_schema: dict[str, Any],
|
|
116
|
+
*,
|
|
117
|
+
output_schema: dict[str, Any] | None = None,
|
|
118
|
+
annotations: ToolAnnotations | None = None,
|
|
119
|
+
options: BundledOptions | None = None,
|
|
120
|
+
) -> ToolSpec:
|
|
121
|
+
"""What the framework declares, overridden by ``options``."""
|
|
122
|
+
options = options or BundledOptions()
|
|
123
|
+
return ToolSpec(
|
|
124
|
+
name=name,
|
|
125
|
+
description=description,
|
|
126
|
+
input_schema=input_schema,
|
|
127
|
+
output_schema=output_schema,
|
|
128
|
+
summary=options.summary,
|
|
129
|
+
annotations=options.annotations or annotations,
|
|
130
|
+
display=options.display,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def bundled_tool(spec: ToolSpec, execute: ExecuteFn) -> Tool:
|
|
135
|
+
"""Internal: the definition adapters publish for a framework tool. ``execute`` runs it for a
|
|
136
|
+
``tools.execute`` routed here and must audit the call itself, exactly as a direct call is."""
|
|
137
|
+
return Tool(
|
|
138
|
+
description=spec.description,
|
|
139
|
+
input_schema=spec.input_schema,
|
|
140
|
+
_execute=execute,
|
|
141
|
+
output_schema=spec.output_schema,
|
|
142
|
+
summary=spec.summary,
|
|
143
|
+
annotations=spec.annotations,
|
|
144
|
+
display=spec.display,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_MODEL_INVALID = re.compile(r"[^a-zA-Z0-9_-]")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def model_tool_name(name: str, invalid: re.Pattern[str] = _MODEL_INVALID) -> str:
|
|
152
|
+
"""The name a framework shows the model for a Tilde tool: ``name`` with characters matching
|
|
153
|
+
``invalid`` (the framework's disallowed characters) replaced by ``_``. Catalog names
|
|
154
|
+
(``{source}.{tool}``) can exceed the 64 characters models accept, so a longer result is
|
|
155
|
+
truncated and suffixed with a hash of ``name``, keeping it stable and distinct from other
|
|
156
|
+
long names sharing its start. Adapters bind each alias to its catalog tool when converting.
|
|
157
|
+
Matches ``modelToolName`` in the TypeScript SDK."""
|
|
158
|
+
key = invalid.sub("_", name)
|
|
159
|
+
if len(key) <= 64:
|
|
160
|
+
return key
|
|
161
|
+
return f"{key[:55]}_{hashlib.sha256(name.encode()).hexdigest()[:8]}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def tool_id(invocation: str, call: str, name: str) -> str:
|
|
165
|
+
"""Stable UUID derived from the invocation and the framework's tool-call ID."""
|
|
166
|
+
if not call:
|
|
167
|
+
raise ValueError("Tool call ID is required")
|
|
168
|
+
h = hashlib.sha256(f"{invocation}:{call}:{name}".encode()).hexdigest()
|
|
169
|
+
return f"{h[:8]}-{h[8:12]}-4{h[13:16]}-a{h[17:20]}-{h[20:32]}"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def as_object(input: Any) -> dict[str, Any]:
|
|
173
|
+
if not isinstance(input, dict):
|
|
174
|
+
raise ValueError("Tool input must be an object")
|
|
175
|
+
return input
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def as_string(input: dict[str, Any], key: str) -> str:
|
|
179
|
+
value = input.get(key)
|
|
180
|
+
if not isinstance(value, str):
|
|
181
|
+
raise ValueError(f"{key} must be a string")
|
|
182
|
+
return value
|
tilde/_transport.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""One HTTP/2 transport per process for callbacks, matching the Node SDK's HTTP/2 clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pyqwest
|
|
6
|
+
|
|
7
|
+
_transport: pyqwest.HTTPTransport | None = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def http_client() -> pyqwest.Client:
|
|
11
|
+
global _transport
|
|
12
|
+
if _transport is None:
|
|
13
|
+
# Plaintext callback URLs use HTTP/2 prior knowledge; TLS negotiates HTTP/2 through ALPN.
|
|
14
|
+
# Redirects are refused so bearer credentials never follow an upstream redirect.
|
|
15
|
+
_transport = pyqwest.HTTPTransport(
|
|
16
|
+
http_version=pyqwest.HTTPVersion.HTTP2,
|
|
17
|
+
follow_redirects=False,
|
|
18
|
+
tls_include_system_certs=True,
|
|
19
|
+
)
|
|
20
|
+
return pyqwest.Client(_transport)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def close_http_client() -> None:
|
|
24
|
+
"""Close pooled callback connections, for example before a test server shuts down."""
|
|
25
|
+
global _transport
|
|
26
|
+
transport, _transport = _transport, None
|
|
27
|
+
if transport is not None:
|
|
28
|
+
await transport.aclose()
|
|
File without changes
|
|
File without changes
|