runlet 0.1.0__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.
- runlet/__init__.py +87 -0
- runlet/core/__init__.py +52 -0
- runlet/core/agent.py +18 -0
- runlet/core/errors.py +34 -0
- runlet/core/events.py +51 -0
- runlet/core/messages.py +47 -0
- runlet/core/models.py +130 -0
- runlet/core/runs.py +82 -0
- runlet/integrations/__init__.py +13 -0
- runlet/integrations/hooks.py +50 -0
- runlet/integrations/tools.py +77 -0
- runlet/providers/__init__.py +5 -0
- runlet/providers/openai.py +156 -0
- runlet/py.typed +1 -0
- runlet/runtime/__init__.py +20 -0
- runlet/runtime/context.py +48 -0
- runlet/runtime/engine.py +255 -0
- runlet/runtime/policies.py +23 -0
- runlet/runtime/state.py +29 -0
- runlet/testing/__init__.py +6 -0
- runlet/testing/fakes.py +56 -0
- runlet-0.1.0.dist-info/METADATA +256 -0
- runlet-0.1.0.dist-info/RECORD +25 -0
- runlet-0.1.0.dist-info/WHEEL +4 -0
- runlet-0.1.0.dist-info/licenses/LICENSE +22 -0
runlet/__init__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Runlet: a tiny observable runtime for Python agents."""
|
|
2
|
+
|
|
3
|
+
from runlet.core import (
|
|
4
|
+
Agent,
|
|
5
|
+
CancellationError,
|
|
6
|
+
CompositeEventSink,
|
|
7
|
+
ContextOverflowError,
|
|
8
|
+
InMemoryObserver,
|
|
9
|
+
HookError,
|
|
10
|
+
InternalRuntimeError,
|
|
11
|
+
Message,
|
|
12
|
+
ModelCapabilities,
|
|
13
|
+
ModelError,
|
|
14
|
+
ModelRequest,
|
|
15
|
+
ModelResponse,
|
|
16
|
+
ModelStreamEvent,
|
|
17
|
+
ProviderStreamEvent,
|
|
18
|
+
PolicyStop,
|
|
19
|
+
RunContext,
|
|
20
|
+
RunResult,
|
|
21
|
+
RunletError,
|
|
22
|
+
RuntimeEvent,
|
|
23
|
+
StateError,
|
|
24
|
+
ToolCall,
|
|
25
|
+
ToolError,
|
|
26
|
+
ToolResult,
|
|
27
|
+
Usage,
|
|
28
|
+
)
|
|
29
|
+
from runlet.integrations import BaseHook, HookRunner, ToolContext, ToolSpec, execute_tool_call, tool
|
|
30
|
+
from runlet.runtime import (
|
|
31
|
+
ContextManager,
|
|
32
|
+
ContextPolicy,
|
|
33
|
+
HookPolicy,
|
|
34
|
+
InMemoryStateStore,
|
|
35
|
+
RunPolicy,
|
|
36
|
+
Runtime,
|
|
37
|
+
SimpleTokenEstimator,
|
|
38
|
+
StateScope,
|
|
39
|
+
TokenEstimate,
|
|
40
|
+
ToolPolicy,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0"
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"Agent",
|
|
47
|
+
"CancellationError",
|
|
48
|
+
"CompositeEventSink",
|
|
49
|
+
"ContextManager",
|
|
50
|
+
"ContextPolicy",
|
|
51
|
+
"ContextOverflowError",
|
|
52
|
+
"BaseHook",
|
|
53
|
+
"HookError",
|
|
54
|
+
"HookRunner",
|
|
55
|
+
"HookPolicy",
|
|
56
|
+
"InMemoryObserver",
|
|
57
|
+
"InternalRuntimeError",
|
|
58
|
+
"Message",
|
|
59
|
+
"ModelCapabilities",
|
|
60
|
+
"ModelError",
|
|
61
|
+
"ModelRequest",
|
|
62
|
+
"ModelResponse",
|
|
63
|
+
"ModelStreamEvent",
|
|
64
|
+
"ProviderStreamEvent",
|
|
65
|
+
"PolicyStop",
|
|
66
|
+
"RunContext",
|
|
67
|
+
"RunPolicy",
|
|
68
|
+
"RunResult",
|
|
69
|
+
"RunletError",
|
|
70
|
+
"Runtime",
|
|
71
|
+
"RuntimeEvent",
|
|
72
|
+
"SimpleTokenEstimator",
|
|
73
|
+
"StateScope",
|
|
74
|
+
"StateError",
|
|
75
|
+
"InMemoryStateStore",
|
|
76
|
+
"TokenEstimate",
|
|
77
|
+
"ToolCall",
|
|
78
|
+
"ToolContext",
|
|
79
|
+
"ToolError",
|
|
80
|
+
"ToolResult",
|
|
81
|
+
"ToolPolicy",
|
|
82
|
+
"ToolSpec",
|
|
83
|
+
"Usage",
|
|
84
|
+
"__version__",
|
|
85
|
+
"execute_tool_call",
|
|
86
|
+
"tool",
|
|
87
|
+
]
|
runlet/core/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from runlet.core.agent import Agent
|
|
2
|
+
from runlet.core.errors import (
|
|
3
|
+
CancellationError,
|
|
4
|
+
ContextOverflowError,
|
|
5
|
+
HookError,
|
|
6
|
+
InternalRuntimeError,
|
|
7
|
+
ModelError,
|
|
8
|
+
PolicyStop,
|
|
9
|
+
RunletError,
|
|
10
|
+
StateError,
|
|
11
|
+
ToolError,
|
|
12
|
+
)
|
|
13
|
+
from runlet.core.events import CompositeEventSink, EventSink, InMemoryObserver, RuntimeEvent
|
|
14
|
+
from runlet.core.messages import Message, ToolCall, ToolResult
|
|
15
|
+
from runlet.core.models import (
|
|
16
|
+
ModelCapabilities,
|
|
17
|
+
ModelProvider,
|
|
18
|
+
ModelRequest,
|
|
19
|
+
ModelResponse,
|
|
20
|
+
ModelStreamEvent,
|
|
21
|
+
ProviderStreamEvent,
|
|
22
|
+
)
|
|
23
|
+
from runlet.core.runs import RunContext, RunResult, Usage
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Agent",
|
|
27
|
+
"CancellationError",
|
|
28
|
+
"CompositeEventSink",
|
|
29
|
+
"ContextOverflowError",
|
|
30
|
+
"EventSink",
|
|
31
|
+
"HookError",
|
|
32
|
+
"InMemoryObserver",
|
|
33
|
+
"InternalRuntimeError",
|
|
34
|
+
"Message",
|
|
35
|
+
"ModelCapabilities",
|
|
36
|
+
"ModelError",
|
|
37
|
+
"ModelProvider",
|
|
38
|
+
"ModelRequest",
|
|
39
|
+
"ModelResponse",
|
|
40
|
+
"ModelStreamEvent",
|
|
41
|
+
"ProviderStreamEvent",
|
|
42
|
+
"PolicyStop",
|
|
43
|
+
"RunContext",
|
|
44
|
+
"RunResult",
|
|
45
|
+
"RunletError",
|
|
46
|
+
"RuntimeEvent",
|
|
47
|
+
"StateError",
|
|
48
|
+
"ToolCall",
|
|
49
|
+
"ToolError",
|
|
50
|
+
"ToolResult",
|
|
51
|
+
"Usage",
|
|
52
|
+
]
|
runlet/core/agent.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _metadata_map() -> dict[str, Any]:
|
|
8
|
+
return {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Agent:
|
|
13
|
+
name: str
|
|
14
|
+
instructions: str
|
|
15
|
+
model: Any
|
|
16
|
+
tools: tuple[Any, ...] = ()
|
|
17
|
+
hooks: tuple[Any, ...] = ()
|
|
18
|
+
metadata: dict[str, Any] = field(default_factory=_metadata_map)
|
runlet/core/errors.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class RunletError(Exception):
|
|
2
|
+
code = "runlet_error"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ModelError(RunletError):
|
|
6
|
+
code = "model_error"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ToolError(RunletError):
|
|
10
|
+
code = "tool_error"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContextOverflowError(RunletError):
|
|
14
|
+
code = "context_overflow"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HookError(RunletError):
|
|
18
|
+
code = "hook_error"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PolicyStop(RunletError):
|
|
22
|
+
code = "policy_stop"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StateError(RunletError):
|
|
26
|
+
code = "state_error"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CancellationError(RunletError):
|
|
30
|
+
code = "cancelled"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InternalRuntimeError(RunletError):
|
|
34
|
+
code = "internal_runtime_error"
|
runlet/core/events.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def utc_now() -> datetime:
|
|
10
|
+
return datetime.now(timezone.utc)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _event_map() -> dict[str, Any]:
|
|
14
|
+
return {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class RuntimeEvent:
|
|
19
|
+
type: str
|
|
20
|
+
run_id: str
|
|
21
|
+
id: str = field(default_factory=lambda: f"evt_{uuid4().hex}")
|
|
22
|
+
timestamp: datetime = field(default_factory=utc_now)
|
|
23
|
+
step_id: str | None = None
|
|
24
|
+
span_id: str | None = None
|
|
25
|
+
parent_span_id: str | None = None
|
|
26
|
+
agent_name: str | None = None
|
|
27
|
+
severity: str = "info"
|
|
28
|
+
payload: dict[str, Any] = field(default_factory=_event_map)
|
|
29
|
+
attributes: dict[str, Any] = field(default_factory=_event_map)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EventSink(Protocol):
|
|
33
|
+
async def emit(self, event: RuntimeEvent) -> None:
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InMemoryObserver:
|
|
38
|
+
def __init__(self) -> None:
|
|
39
|
+
self.events: list[RuntimeEvent] = []
|
|
40
|
+
|
|
41
|
+
async def emit(self, event: RuntimeEvent) -> None:
|
|
42
|
+
self.events.append(event)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CompositeEventSink:
|
|
46
|
+
def __init__(self, sinks: list[EventSink] | tuple[EventSink, ...]) -> None:
|
|
47
|
+
self.sinks = tuple(sinks)
|
|
48
|
+
|
|
49
|
+
async def emit(self, event: RuntimeEvent) -> None:
|
|
50
|
+
for sink in self.sinks:
|
|
51
|
+
await sink.emit(event)
|
runlet/core/messages.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _string_map() -> dict[str, Any]:
|
|
8
|
+
return {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Message:
|
|
13
|
+
role: str
|
|
14
|
+
text: str
|
|
15
|
+
metadata: dict[str, Any] = field(default_factory=_string_map)
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def system(cls, text: str, metadata: dict[str, Any] | None = None) -> "Message":
|
|
19
|
+
return cls(role="system", text=text, metadata=metadata or {})
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def user(cls, text: str, metadata: dict[str, Any] | None = None) -> "Message":
|
|
23
|
+
return cls(role="user", text=text, metadata=metadata or {})
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def assistant(cls, text: str, metadata: dict[str, Any] | None = None) -> "Message":
|
|
27
|
+
return cls(role="assistant", text=text, metadata=metadata or {})
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def tool(cls, text: str, metadata: dict[str, Any] | None = None) -> "Message":
|
|
31
|
+
return cls(role="tool", text=text, metadata=metadata or {})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class ToolCall:
|
|
36
|
+
id: str
|
|
37
|
+
name: str
|
|
38
|
+
arguments: dict[str, Any] = field(default_factory=_string_map)
|
|
39
|
+
metadata: dict[str, Any] = field(default_factory=_string_map)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ToolResult:
|
|
44
|
+
call_id: str
|
|
45
|
+
name: str
|
|
46
|
+
content: str
|
|
47
|
+
metadata: dict[str, Any] = field(default_factory=_string_map)
|
runlet/core/models.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Protocol
|
|
6
|
+
|
|
7
|
+
from runlet.core.messages import Message, ToolCall
|
|
8
|
+
from runlet.core.runs import Usage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _tool_list() -> list[Any]:
|
|
12
|
+
return []
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _metadata_map() -> dict[str, Any]:
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _tool_call_list() -> list[ToolCall]:
|
|
20
|
+
return []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ModelCapabilities:
|
|
25
|
+
model_name: str
|
|
26
|
+
context_window: int
|
|
27
|
+
supports_tools: bool = True
|
|
28
|
+
supports_parallel_tool_calls: bool = False
|
|
29
|
+
supports_streaming: bool = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ModelRequest:
|
|
34
|
+
messages: list[Message]
|
|
35
|
+
tools: list[Any] = field(default_factory=_tool_list)
|
|
36
|
+
metadata: dict[str, Any] = field(default_factory=_metadata_map)
|
|
37
|
+
options: dict[str, Any] = field(default_factory=_metadata_map)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class ModelResponse:
|
|
42
|
+
message: Message
|
|
43
|
+
tool_calls: list[ToolCall] = field(default_factory=_tool_call_list)
|
|
44
|
+
usage: Usage = field(default_factory=Usage)
|
|
45
|
+
final: bool = True
|
|
46
|
+
raw: Any = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class ModelStreamEvent:
|
|
51
|
+
delta: str = ""
|
|
52
|
+
tool_call: ToolCall | None = None
|
|
53
|
+
usage: Usage | None = None
|
|
54
|
+
final: bool = False
|
|
55
|
+
raw: Any = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _arguments_map() -> dict[str, Any]:
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class ProviderStreamEvent:
|
|
64
|
+
kind: str
|
|
65
|
+
delta: str = ""
|
|
66
|
+
call_id: str | None = None
|
|
67
|
+
name: str | None = None
|
|
68
|
+
arguments_delta: str = ""
|
|
69
|
+
arguments: dict[str, Any] = field(default_factory=_arguments_map)
|
|
70
|
+
usage: Usage | None = None
|
|
71
|
+
raw: Any = None
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def text_delta(cls, delta: str, raw: Any = None) -> "ProviderStreamEvent":
|
|
75
|
+
return cls(kind="text_delta", delta=delta, raw=raw)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def tool_call_delta(
|
|
79
|
+
cls,
|
|
80
|
+
call_id: str,
|
|
81
|
+
name: str | None,
|
|
82
|
+
arguments_delta: str,
|
|
83
|
+
raw: Any = None,
|
|
84
|
+
) -> "ProviderStreamEvent":
|
|
85
|
+
return cls(
|
|
86
|
+
kind="tool_call_delta",
|
|
87
|
+
call_id=call_id,
|
|
88
|
+
name=name,
|
|
89
|
+
arguments_delta=arguments_delta,
|
|
90
|
+
raw=raw,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def tool_call_completed(
|
|
95
|
+
cls,
|
|
96
|
+
call_id: str,
|
|
97
|
+
name: str,
|
|
98
|
+
arguments: dict[str, Any],
|
|
99
|
+
raw: Any = None,
|
|
100
|
+
) -> "ProviderStreamEvent":
|
|
101
|
+
return cls(
|
|
102
|
+
kind="tool_call_completed",
|
|
103
|
+
call_id=call_id,
|
|
104
|
+
name=name,
|
|
105
|
+
arguments=dict(arguments),
|
|
106
|
+
raw=raw,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def message_completed(cls, raw: Any = None) -> "ProviderStreamEvent":
|
|
111
|
+
return cls(kind="message_completed", raw=raw)
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def usage_event(cls, usage: Usage, raw: Any = None) -> "ProviderStreamEvent":
|
|
115
|
+
return cls(kind="usage", usage=usage, raw=raw)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def completed(cls, raw: Any = None) -> "ProviderStreamEvent":
|
|
119
|
+
return cls(kind="completed", raw=raw)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ModelProvider(Protocol):
|
|
123
|
+
async def complete(self, request: ModelRequest) -> ModelResponse:
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
async def stream(self, request: ModelRequest) -> AsyncIterator[ProviderStreamEvent | ModelStreamEvent]:
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
async def capabilities(self) -> ModelCapabilities:
|
|
130
|
+
...
|
runlet/core/runs.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from runlet.core.agent import Agent
|
|
7
|
+
from runlet.core.messages import Message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _message_list() -> list[Message]:
|
|
11
|
+
return []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _metadata_map() -> dict[str, Any]:
|
|
15
|
+
return {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _state_map() -> dict[str, Any]:
|
|
19
|
+
return {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Usage:
|
|
24
|
+
input_tokens: int = 0
|
|
25
|
+
output_tokens: int = 0
|
|
26
|
+
source: str = "actual"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def total_tokens(self) -> int:
|
|
30
|
+
return self.input_tokens + self.output_tokens
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class RunContext:
|
|
35
|
+
run_id: str
|
|
36
|
+
agent: Agent
|
|
37
|
+
messages: list[Message] = field(default_factory=_message_list)
|
|
38
|
+
metadata: dict[str, Any] = field(default_factory=_metadata_map)
|
|
39
|
+
state: dict[str, Any] = field(default_factory=_state_map)
|
|
40
|
+
usage: Usage = field(default_factory=Usage)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class RunResult:
|
|
45
|
+
run_id: str
|
|
46
|
+
status: str
|
|
47
|
+
output: str | None = None
|
|
48
|
+
messages: tuple[Message, ...] = ()
|
|
49
|
+
usage: Usage = field(default_factory=Usage)
|
|
50
|
+
error: str | None = None
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def completed(
|
|
54
|
+
cls,
|
|
55
|
+
run_id: str,
|
|
56
|
+
output: str,
|
|
57
|
+
usage: Usage | None = None,
|
|
58
|
+
messages: tuple[Message, ...] = (),
|
|
59
|
+
) -> "RunResult":
|
|
60
|
+
return cls(
|
|
61
|
+
run_id=run_id,
|
|
62
|
+
status="completed",
|
|
63
|
+
output=output,
|
|
64
|
+
usage=usage or Usage(),
|
|
65
|
+
messages=messages,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def failed(
|
|
70
|
+
cls,
|
|
71
|
+
run_id: str,
|
|
72
|
+
error: str,
|
|
73
|
+
usage: Usage | None = None,
|
|
74
|
+
messages: tuple[Message, ...] = (),
|
|
75
|
+
) -> "RunResult":
|
|
76
|
+
return cls(
|
|
77
|
+
run_id=run_id,
|
|
78
|
+
status="failed",
|
|
79
|
+
usage=usage or Usage(),
|
|
80
|
+
error=error,
|
|
81
|
+
messages=messages,
|
|
82
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from runlet.integrations.hooks import BaseHook, HookRunner
|
|
2
|
+
from runlet.integrations.tools import ToolContext, ToolHandler, ToolSpec, execute_tool_call, tool, validate_arguments
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"BaseHook",
|
|
6
|
+
"HookRunner",
|
|
7
|
+
"ToolContext",
|
|
8
|
+
"ToolHandler",
|
|
9
|
+
"ToolSpec",
|
|
10
|
+
"execute_tool_call",
|
|
11
|
+
"tool",
|
|
12
|
+
"validate_arguments",
|
|
13
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseHook:
|
|
7
|
+
enabled = True
|
|
8
|
+
|
|
9
|
+
def is_enabled(self, context: Any) -> bool:
|
|
10
|
+
return bool(self.enabled)
|
|
11
|
+
|
|
12
|
+
async def before_model_request(self, request: Any, context: Any) -> Any:
|
|
13
|
+
return request
|
|
14
|
+
|
|
15
|
+
async def after_model_response(self, response: Any, context: Any) -> Any:
|
|
16
|
+
return response
|
|
17
|
+
|
|
18
|
+
async def before_tool_call(self, call: Any, context: Any) -> Any:
|
|
19
|
+
return call
|
|
20
|
+
|
|
21
|
+
async def after_tool_result(self, result: Any, context: Any) -> Any:
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HookRunner:
|
|
26
|
+
def __init__(self, hooks: list[BaseHook] | tuple[BaseHook, ...]) -> None:
|
|
27
|
+
self.hooks = tuple(hooks)
|
|
28
|
+
|
|
29
|
+
def _enabled_hooks(self, context: Any) -> list[BaseHook]:
|
|
30
|
+
return [hook for hook in self.hooks if hook.is_enabled(context)]
|
|
31
|
+
|
|
32
|
+
async def before_model_request(self, request: Any, context: Any) -> Any:
|
|
33
|
+
for hook in self._enabled_hooks(context):
|
|
34
|
+
request = await hook.before_model_request(request, context)
|
|
35
|
+
return request
|
|
36
|
+
|
|
37
|
+
async def after_model_response(self, response: Any, context: Any) -> Any:
|
|
38
|
+
for hook in self._enabled_hooks(context):
|
|
39
|
+
response = await hook.after_model_response(response, context)
|
|
40
|
+
return response
|
|
41
|
+
|
|
42
|
+
async def before_tool_call(self, call: Any, context: Any) -> Any:
|
|
43
|
+
for hook in self._enabled_hooks(context):
|
|
44
|
+
call = await hook.before_tool_call(call, context)
|
|
45
|
+
return call
|
|
46
|
+
|
|
47
|
+
async def after_tool_result(self, result: Any, context: Any) -> Any:
|
|
48
|
+
for hook in self._enabled_hooks(context):
|
|
49
|
+
result = await hook.after_tool_result(result, context)
|
|
50
|
+
return result
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from runlet.core.messages import ToolCall, ToolResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ToolHandler = Callable[[dict[str, Any], "ToolContext"], Awaitable[str]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _metadata_map() -> dict[str, Any]:
|
|
14
|
+
return {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ToolContext:
|
|
19
|
+
run_id: str
|
|
20
|
+
metadata: dict[str, Any] = field(default_factory=_metadata_map)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class ToolSpec:
|
|
25
|
+
name: str
|
|
26
|
+
description: str
|
|
27
|
+
input_schema: dict[str, Any]
|
|
28
|
+
handler: ToolHandler
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _annotation_to_schema(annotation: Any) -> dict[str, str]:
|
|
32
|
+
if annotation is int:
|
|
33
|
+
return {"type": "integer"}
|
|
34
|
+
if annotation is float:
|
|
35
|
+
return {"type": "number"}
|
|
36
|
+
if annotation is bool:
|
|
37
|
+
return {"type": "boolean"}
|
|
38
|
+
return {"type": "string"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def tool(func: Callable[..., Awaitable[str]]) -> ToolSpec:
|
|
42
|
+
signature = inspect.signature(func)
|
|
43
|
+
properties: dict[str, dict[str, str]] = {}
|
|
44
|
+
required: list[str] = []
|
|
45
|
+
for name, parameter in signature.parameters.items():
|
|
46
|
+
required.append(name)
|
|
47
|
+
properties[name] = _annotation_to_schema(parameter.annotation)
|
|
48
|
+
|
|
49
|
+
async def handler(arguments: dict[str, Any], context: ToolContext) -> str:
|
|
50
|
+
del context
|
|
51
|
+
return await func(**arguments)
|
|
52
|
+
|
|
53
|
+
return ToolSpec(
|
|
54
|
+
name=func.__name__,
|
|
55
|
+
description=(func.__doc__ or "").strip(),
|
|
56
|
+
input_schema={"type": "object", "required": required, "properties": properties},
|
|
57
|
+
handler=handler,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate_arguments(schema: dict[str, Any], arguments: dict[str, Any]) -> None:
|
|
62
|
+
for name in schema.get("required", []):
|
|
63
|
+
if name not in arguments:
|
|
64
|
+
raise ValueError(f"Missing required tool argument: {name}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def execute_tool_call(
|
|
68
|
+
call: ToolCall,
|
|
69
|
+
tools: dict[str, ToolSpec],
|
|
70
|
+
context: ToolContext,
|
|
71
|
+
) -> ToolResult:
|
|
72
|
+
if call.name not in tools:
|
|
73
|
+
raise ValueError(f"Tool not found: {call.name}")
|
|
74
|
+
spec = tools[call.name]
|
|
75
|
+
validate_arguments(spec.input_schema, call.arguments)
|
|
76
|
+
content = await spec.handler(call.arguments, context)
|
|
77
|
+
return ToolResult(call_id=call.id, name=call.name, content=str(content))
|