ariacompute-agent 0.17.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.
- ariacompute_agent-0.17.0/PKG-INFO +95 -0
- ariacompute_agent-0.17.0/README.md +84 -0
- ariacompute_agent-0.17.0/ariacompute_agent/__init__.py +75 -0
- ariacompute_agent-0.17.0/ariacompute_agent/agent.py +83 -0
- ariacompute_agent-0.17.0/ariacompute_agent/memory.py +216 -0
- ariacompute_agent-0.17.0/ariacompute_agent/runner.py +153 -0
- ariacompute_agent-0.17.0/ariacompute_agent/session.py +104 -0
- ariacompute_agent-0.17.0/ariacompute_agent/tool.py +86 -0
- ariacompute_agent-0.17.0/ariacompute_agent/transport.py +125 -0
- ariacompute_agent-0.17.0/ariacompute_agent/types.py +89 -0
- ariacompute_agent-0.17.0/ariacompute_agent.egg-info/PKG-INFO +95 -0
- ariacompute_agent-0.17.0/ariacompute_agent.egg-info/SOURCES.txt +17 -0
- ariacompute_agent-0.17.0/ariacompute_agent.egg-info/dependency_links.txt +1 -0
- ariacompute_agent-0.17.0/ariacompute_agent.egg-info/requires.txt +3 -0
- ariacompute_agent-0.17.0/ariacompute_agent.egg-info/top_level.txt +1 -0
- ariacompute_agent-0.17.0/pyproject.toml +22 -0
- ariacompute_agent-0.17.0/setup.cfg +4 -0
- ariacompute_agent-0.17.0/tests/test_memory.py +211 -0
- ariacompute_agent-0.17.0/tests/test_sdk.py +232 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ariacompute-agent
|
|
3
|
+
Version: 0.17.0
|
|
4
|
+
Summary: Aria agent SDK - mirrors the OpenAI Agents SDK (Agent / Runner.run / Runner.run_streamed / function_tool / Session) on top of the aria-agent-cloud beta Agents API.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: agent,agents-sdk,aria,ariacompute,openai
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == "dev"
|
|
11
|
+
|
|
12
|
+
# `ariacompute-agent`
|
|
13
|
+
|
|
14
|
+
The Aria agent SDK for Python. The API mirrors the
|
|
15
|
+
[OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents/quickstart)
|
|
16
|
+
(`Agent`, `Runner.run`, `Runner.run_streamed`, `function_tool`, `Session`) and
|
|
17
|
+
talks to an `aria-agent-cloud` deployment through its **beta Agents** REST API.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install ariacompute-agent
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
|
|
26
|
+
from ariacompute_agent import Agent, Runner
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def main() -> None:
|
|
30
|
+
agent = Agent(
|
|
31
|
+
name="History tutor",
|
|
32
|
+
instructions="Answer history questions clearly and concisely.",
|
|
33
|
+
model="gpt-4o-mini",
|
|
34
|
+
)
|
|
35
|
+
result = await Runner.run(agent, "When did the Roman Empire fall?")
|
|
36
|
+
print(result.final_output)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
asyncio.run(main())
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Tools
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from ariacompute_agent import function_tool
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@function_tool
|
|
49
|
+
def history_fun_fact() -> str:
|
|
50
|
+
"""Return a short history fact."""
|
|
51
|
+
return "Sharks are older than trees."
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
agent = Agent(name="History tutor", tools=[history_fun_fact])
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The cloud executes tools inside its own sandbox, so only the schema
|
|
58
|
+
(`name` / `description` / `parameters`) is sent to the server.
|
|
59
|
+
|
|
60
|
+
## Streaming
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
streamed = await Runner.run_streamed(agent, "Tell me something surprising")
|
|
64
|
+
async for ev in streamed.events:
|
|
65
|
+
if ev["type"] == "agent.turn.output_text.delta":
|
|
66
|
+
print(ev["delta"], end="")
|
|
67
|
+
result = await streamed.completed
|
|
68
|
+
print(result.final_output)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Frames use the `agent.*` envelope; `agent.turn.completed` (or
|
|
72
|
+
`agent.turn.failed`) is the **only** terminal event — there is no
|
|
73
|
+
`data: [DONE]` sentinel.
|
|
74
|
+
|
|
75
|
+
## Sessions
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
session = Session.create(agent)
|
|
79
|
+
await Runner.run(agent, "hello", session=session)
|
|
80
|
+
await Runner.run(agent, "and then?", session=session)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
| Option | Environment variable | Default |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| `base_url` | `ARIA_AGENT_BASE_URL` | `http://localhost:3000` |
|
|
88
|
+
| `api_key` | `ARIA_AGENT_API_KEY` | _(none)_ |
|
|
89
|
+
| `beta_header` | – | `agents=v1` (`OpenAI-Beta`) |
|
|
90
|
+
|
|
91
|
+
## Tests
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python3 -m unittest discover -s tests
|
|
95
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# `ariacompute-agent`
|
|
2
|
+
|
|
3
|
+
The Aria agent SDK for Python. The API mirrors the
|
|
4
|
+
[OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents/quickstart)
|
|
5
|
+
(`Agent`, `Runner.run`, `Runner.run_streamed`, `function_tool`, `Session`) and
|
|
6
|
+
talks to an `aria-agent-cloud` deployment through its **beta Agents** REST API.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install ariacompute-agent
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import asyncio
|
|
14
|
+
|
|
15
|
+
from ariacompute_agent import Agent, Runner
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def main() -> None:
|
|
19
|
+
agent = Agent(
|
|
20
|
+
name="History tutor",
|
|
21
|
+
instructions="Answer history questions clearly and concisely.",
|
|
22
|
+
model="gpt-4o-mini",
|
|
23
|
+
)
|
|
24
|
+
result = await Runner.run(agent, "When did the Roman Empire fall?")
|
|
25
|
+
print(result.final_output)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
asyncio.run(main())
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Tools
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from ariacompute_agent import function_tool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@function_tool
|
|
38
|
+
def history_fun_fact() -> str:
|
|
39
|
+
"""Return a short history fact."""
|
|
40
|
+
return "Sharks are older than trees."
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
agent = Agent(name="History tutor", tools=[history_fun_fact])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The cloud executes tools inside its own sandbox, so only the schema
|
|
47
|
+
(`name` / `description` / `parameters`) is sent to the server.
|
|
48
|
+
|
|
49
|
+
## Streaming
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
streamed = await Runner.run_streamed(agent, "Tell me something surprising")
|
|
53
|
+
async for ev in streamed.events:
|
|
54
|
+
if ev["type"] == "agent.turn.output_text.delta":
|
|
55
|
+
print(ev["delta"], end="")
|
|
56
|
+
result = await streamed.completed
|
|
57
|
+
print(result.final_output)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Frames use the `agent.*` envelope; `agent.turn.completed` (or
|
|
61
|
+
`agent.turn.failed`) is the **only** terminal event — there is no
|
|
62
|
+
`data: [DONE]` sentinel.
|
|
63
|
+
|
|
64
|
+
## Sessions
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
session = Session.create(agent)
|
|
68
|
+
await Runner.run(agent, "hello", session=session)
|
|
69
|
+
await Runner.run(agent, "and then?", session=session)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
| Option | Environment variable | Default |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `base_url` | `ARIA_AGENT_BASE_URL` | `http://localhost:3000` |
|
|
77
|
+
| `api_key` | `ARIA_AGENT_API_KEY` | _(none)_ |
|
|
78
|
+
| `beta_header` | – | `agents=v1` (`OpenAI-Beta`) |
|
|
79
|
+
|
|
80
|
+
## Tests
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python3 -m unittest discover -s tests
|
|
84
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""``ariacompute-agent`` — the Aria agent SDK.
|
|
2
|
+
|
|
3
|
+
The API mirrors the OpenAI Agents SDK (``openai-agents``) so the official
|
|
4
|
+
quickstart transfers unchanged:
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
from ariacompute_agent import Agent, Runner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def main() -> None:
|
|
13
|
+
agent = Agent(
|
|
14
|
+
name="History tutor",
|
|
15
|
+
instructions="Answer history questions clearly and concisely.",
|
|
16
|
+
model="gpt-4o-mini",
|
|
17
|
+
)
|
|
18
|
+
result = await Runner.run(agent, "When did the Roman Empire fall?")
|
|
19
|
+
print(result.final_output)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
asyncio.run(main())
|
|
23
|
+
```
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .agent import Agent
|
|
27
|
+
from .memory import (
|
|
28
|
+
CloudMemoryStore,
|
|
29
|
+
CompositeMemoryStore,
|
|
30
|
+
LocalMemoryStore,
|
|
31
|
+
MemoryBackend,
|
|
32
|
+
)
|
|
33
|
+
from .runner import Runner
|
|
34
|
+
from .session import Session
|
|
35
|
+
from .tool import function_tool, tool_from_function
|
|
36
|
+
from .transport import (
|
|
37
|
+
DEFAULT_BETA_HEADER,
|
|
38
|
+
AgentTransportError,
|
|
39
|
+
get_json,
|
|
40
|
+
post_json,
|
|
41
|
+
resolve_client,
|
|
42
|
+
)
|
|
43
|
+
from .types import (
|
|
44
|
+
ClientOptions,
|
|
45
|
+
HistoryInput,
|
|
46
|
+
HistoryItem,
|
|
47
|
+
RunResult,
|
|
48
|
+
StreamedRunResult,
|
|
49
|
+
ToolSpec,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"Agent",
|
|
54
|
+
"Runner",
|
|
55
|
+
"Session",
|
|
56
|
+
"MemoryBackend",
|
|
57
|
+
"LocalMemoryStore",
|
|
58
|
+
"CloudMemoryStore",
|
|
59
|
+
"CompositeMemoryStore",
|
|
60
|
+
"function_tool",
|
|
61
|
+
"tool_from_function",
|
|
62
|
+
"ClientOptions",
|
|
63
|
+
"HistoryItem",
|
|
64
|
+
"HistoryInput",
|
|
65
|
+
"RunResult",
|
|
66
|
+
"StreamedRunResult",
|
|
67
|
+
"ToolSpec",
|
|
68
|
+
"AgentTransportError",
|
|
69
|
+
"resolve_client",
|
|
70
|
+
"post_json",
|
|
71
|
+
"get_json",
|
|
72
|
+
"DEFAULT_BETA_HEADER",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""``Agent`` — mirrors ``Agent(...)`` from the OpenAI Agents SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
from .memory import MemoryBackend
|
|
8
|
+
from .tool import tool_from_function
|
|
9
|
+
from .types import ClientOptions, ToolSpec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Agent:
|
|
13
|
+
"""A reusable agent definition.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
agent = Agent(
|
|
17
|
+
name="History tutor",
|
|
18
|
+
instructions="Answer history questions clearly and concisely.",
|
|
19
|
+
model="gpt-4o-mini",
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
name: str,
|
|
27
|
+
instructions: Optional[str] = None,
|
|
28
|
+
model: Optional[str] = None,
|
|
29
|
+
tools: Optional[Sequence[Any]] = None,
|
|
30
|
+
handoffs: Optional[Sequence["Agent"]] = None,
|
|
31
|
+
handoff_description: Optional[str] = None,
|
|
32
|
+
id: Optional[str] = None,
|
|
33
|
+
client: Optional[ClientOptions] = None,
|
|
34
|
+
memory_backend: Union[str, MemoryBackend, None] = None,
|
|
35
|
+
memo_db: Optional[str] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
if not name or not name.strip():
|
|
38
|
+
raise ValueError("Agent requires a `name`")
|
|
39
|
+
self.name = name
|
|
40
|
+
self.instructions = instructions
|
|
41
|
+
self.model = model
|
|
42
|
+
self.tools = [self._coerce_tool(t) for t in (tools or [])]
|
|
43
|
+
self.handoffs = list(handoffs or [])
|
|
44
|
+
self.handoff_description = handoff_description
|
|
45
|
+
self.id = id
|
|
46
|
+
self.client = client or ClientOptions()
|
|
47
|
+
self.memory_backend = (
|
|
48
|
+
MemoryBackend.parse(memory_backend) if memory_backend is not None
|
|
49
|
+
else MemoryBackend.CLOUD
|
|
50
|
+
)
|
|
51
|
+
self.memo_db = memo_db
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _coerce_tool(t: Any) -> ToolSpec:
|
|
55
|
+
if isinstance(t, ToolSpec):
|
|
56
|
+
return t
|
|
57
|
+
if callable(t):
|
|
58
|
+
return tool_from_function(t)
|
|
59
|
+
raise TypeError(f"unsupported tool: {t!r}")
|
|
60
|
+
|
|
61
|
+
def clone(self, **overrides: Any) -> "Agent":
|
|
62
|
+
"""Return a copy with overridden fields (used by handoffs)."""
|
|
63
|
+
params: dict[str, Any] = dict(
|
|
64
|
+
name=self.name,
|
|
65
|
+
instructions=self.instructions,
|
|
66
|
+
model=self.model,
|
|
67
|
+
tools=self.tools,
|
|
68
|
+
handoffs=self.handoffs,
|
|
69
|
+
handoff_description=self.handoff_description,
|
|
70
|
+
id=self.id,
|
|
71
|
+
client=self.client,
|
|
72
|
+
memory_backend=self.memory_backend,
|
|
73
|
+
memo_db=self.memo_db,
|
|
74
|
+
)
|
|
75
|
+
params.update(overrides)
|
|
76
|
+
return Agent(**params)
|
|
77
|
+
|
|
78
|
+
def tool_schemas(self) -> list[dict[str, Any]]:
|
|
79
|
+
"""JSON-schema tool declarations forwarded to the cloud."""
|
|
80
|
+
return [t.schema() for t in self.tools]
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
|
83
|
+
return f"Agent(name={self.name!r}, model={self.model!r})"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Memory backends for :mod:`ariacompute_agent`.
|
|
2
|
+
|
|
3
|
+
* ``cloud`` — the agent-cloud session memory REST endpoints
|
|
4
|
+
(``/v1/agents/sessions/{id}/memory``).
|
|
5
|
+
* ``local`` — **aria memo**: the same SQLite ``memories`` database the
|
|
6
|
+
``aria-memo`` product/CLI uses, written directly through the stdlib
|
|
7
|
+
:mod:`sqlite3` module (no external binary required). ``aria-memo list --json``
|
|
8
|
+
can read the very same file.
|
|
9
|
+
* ``both`` — writes to local **and** cloud; reads merged (cloud wins, local is
|
|
10
|
+
the fallback).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import sqlite3
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional, Protocol
|
|
20
|
+
|
|
21
|
+
from .transport import get_json, post_json, resolve_client
|
|
22
|
+
from .types import ClientOptions
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MemoryBackend(str, Enum):
|
|
26
|
+
"""Where a memory context lives."""
|
|
27
|
+
|
|
28
|
+
CLOUD = "cloud"
|
|
29
|
+
LOCAL = "local"
|
|
30
|
+
BOTH = "both"
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def parse(cls, value: Optional[object]) -> "MemoryBackend":
|
|
34
|
+
if value is None:
|
|
35
|
+
return cls.CLOUD
|
|
36
|
+
if isinstance(value, cls):
|
|
37
|
+
return value
|
|
38
|
+
raw = str(getattr(value, "value", value)).strip().lower()
|
|
39
|
+
for member in cls:
|
|
40
|
+
if member.value == raw:
|
|
41
|
+
return member
|
|
42
|
+
raise ValueError(f"unknown memory backend: {value}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MemoryStore(Protocol):
|
|
46
|
+
"""Key/value view of a memory backend."""
|
|
47
|
+
|
|
48
|
+
backend: MemoryBackend
|
|
49
|
+
|
|
50
|
+
def put(self, key: str, value: str) -> None: # pragma: no cover - protocol
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def get(self, key: str) -> Optional[str]: # pragma: no cover - protocol
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# --- aria memo (local) -------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
#: aria memo's ``memories`` schema — kept byte-identical so its CLI can open the file.
|
|
60
|
+
ARIA_MEMO_SCHEMA = """
|
|
61
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
memo_type TEXT NOT NULL,
|
|
64
|
+
content TEXT NOT NULL,
|
|
65
|
+
embedding BLOB,
|
|
66
|
+
metadata TEXT NOT NULL,
|
|
67
|
+
importance REAL NOT NULL,
|
|
68
|
+
version INTEGER NOT NULL,
|
|
69
|
+
created_at INTEGER NOT NULL,
|
|
70
|
+
updated_at INTEGER NOT NULL,
|
|
71
|
+
deleted INTEGER NOT NULL DEFAULT 0
|
|
72
|
+
);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memo_type);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_memories_deleted ON memories(deleted);
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
MEMO_TYPE_LONG_TERM = "long_term:semantic"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _now_sec() -> int:
|
|
82
|
+
import time
|
|
83
|
+
|
|
84
|
+
return int(time.time())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LocalMemoryStore:
|
|
88
|
+
"""``local`` backend: aria memo (SQLite, same format as ``aria-memo``)."""
|
|
89
|
+
|
|
90
|
+
backend = MemoryBackend.LOCAL
|
|
91
|
+
|
|
92
|
+
def __init__(self, db_path: Optional[str] = None) -> None:
|
|
93
|
+
path = db_path or __import__("os").environ.get("ARIA_MEMO_DB") or "memo.db"
|
|
94
|
+
self.db_path = str(path)
|
|
95
|
+
parent = Path(self.db_path).parent
|
|
96
|
+
if str(parent) and not parent.exists():
|
|
97
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
self._ensure_schema()
|
|
99
|
+
|
|
100
|
+
def _connect(self) -> sqlite3.Connection:
|
|
101
|
+
conn = sqlite3.connect(self.db_path)
|
|
102
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
103
|
+
return conn
|
|
104
|
+
|
|
105
|
+
def _ensure_schema(self) -> None:
|
|
106
|
+
with self._connect() as conn:
|
|
107
|
+
conn.executescript(ARIA_MEMO_SCHEMA)
|
|
108
|
+
|
|
109
|
+
def put(self, key: str, value: str) -> None:
|
|
110
|
+
"""Write ``key`` as a long-term aria memo (``metadata.key`` holds the key)."""
|
|
111
|
+
now = _now_sec()
|
|
112
|
+
metadata = json.dumps({"key": key})
|
|
113
|
+
with self._connect() as conn:
|
|
114
|
+
conn.execute(
|
|
115
|
+
"INSERT INTO memories "
|
|
116
|
+
"(id, memo_type, content, embedding, metadata, importance, version, "
|
|
117
|
+
" created_at, updated_at, deleted) "
|
|
118
|
+
"VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, 0) "
|
|
119
|
+
"ON CONFLICT(id) DO UPDATE SET "
|
|
120
|
+
" content = excluded.content, metadata = excluded.metadata, "
|
|
121
|
+
" updated_at = excluded.updated_at, deleted = 0",
|
|
122
|
+
(f"key:{key}", MEMO_TYPE_LONG_TERM, value, b"", metadata, 0.8, now, now),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def get(self, key: str) -> Optional[str]:
|
|
126
|
+
with self._connect() as conn:
|
|
127
|
+
rows = conn.execute(
|
|
128
|
+
"SELECT content, metadata FROM memories WHERE deleted = 0",
|
|
129
|
+
).fetchall()
|
|
130
|
+
for content, metadata in rows:
|
|
131
|
+
try:
|
|
132
|
+
parsed = json.loads(metadata or "{}")
|
|
133
|
+
except json.JSONDecodeError:
|
|
134
|
+
parsed = {}
|
|
135
|
+
if parsed.get("key") == key:
|
|
136
|
+
return content
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- cloud -------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class CloudMemoryStore:
|
|
144
|
+
"""``cloud`` backend: the agent-cloud session memory REST endpoints."""
|
|
145
|
+
|
|
146
|
+
backend = MemoryBackend.CLOUD
|
|
147
|
+
|
|
148
|
+
def __init__(self, session_id: str, client: Optional[ClientOptions] = None) -> None:
|
|
149
|
+
self.session_id = session_id
|
|
150
|
+
self.client = client or ClientOptions()
|
|
151
|
+
|
|
152
|
+
def _base(self) -> str:
|
|
153
|
+
return f"/v1/agents/sessions/{self.session_id}/memory"
|
|
154
|
+
|
|
155
|
+
def put(self, key: str, value: str) -> None:
|
|
156
|
+
resolved = resolve_client(self.client)
|
|
157
|
+
post_json(resolved, self._base(), {"key": key, "value": value, "kind": "long_term"})
|
|
158
|
+
|
|
159
|
+
def get(self, key: str) -> Optional[str]:
|
|
160
|
+
resolved = resolve_client(self.client)
|
|
161
|
+
res = get_json(resolved, f"{self._base()}/{key}")
|
|
162
|
+
return res.get("value")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# --- both --------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class CompositeMemoryStore:
|
|
169
|
+
"""``both`` backend: writes to local **and** cloud, reads merged."""
|
|
170
|
+
|
|
171
|
+
backend = MemoryBackend.BOTH
|
|
172
|
+
|
|
173
|
+
def __init__(self, local: MemoryStore, cloud: MemoryStore) -> None:
|
|
174
|
+
self.local = local
|
|
175
|
+
self.cloud = cloud
|
|
176
|
+
|
|
177
|
+
def put(self, key: str, value: str) -> None:
|
|
178
|
+
errors = []
|
|
179
|
+
for store in (self.local, self.cloud):
|
|
180
|
+
try:
|
|
181
|
+
store.put(key, value)
|
|
182
|
+
except Exception as e: # noqa: BLE001 - one side may be offline
|
|
183
|
+
errors.append(str(e))
|
|
184
|
+
if len(errors) == 2:
|
|
185
|
+
raise RuntimeError("memory write failed on every backend: " + "; ".join(errors))
|
|
186
|
+
|
|
187
|
+
def get(self, key: str) -> Optional[str]:
|
|
188
|
+
errors = []
|
|
189
|
+
for store in (self.cloud, self.local):
|
|
190
|
+
try:
|
|
191
|
+
value = store.get(key)
|
|
192
|
+
except Exception as e: # noqa: BLE001 - fall through to the other side
|
|
193
|
+
errors.append(str(e))
|
|
194
|
+
continue
|
|
195
|
+
if value is not None:
|
|
196
|
+
return value
|
|
197
|
+
if len(errors) == 2:
|
|
198
|
+
raise RuntimeError("memory read failed on every backend: " + "; ".join(errors))
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def build_store(
|
|
203
|
+
backend: MemoryBackend,
|
|
204
|
+
session_id: str,
|
|
205
|
+
client: Optional[ClientOptions] = None,
|
|
206
|
+
memo_db: Optional[str] = None,
|
|
207
|
+
) -> MemoryStore:
|
|
208
|
+
"""Build the store for ``backend`` (local is created lazily but validated)."""
|
|
209
|
+
if backend is MemoryBackend.LOCAL:
|
|
210
|
+
return LocalMemoryStore(memo_db)
|
|
211
|
+
if backend is MemoryBackend.CLOUD:
|
|
212
|
+
return CloudMemoryStore(session_id, client)
|
|
213
|
+
return CompositeMemoryStore(
|
|
214
|
+
LocalMemoryStore(memo_db),
|
|
215
|
+
CloudMemoryStore(session_id, client),
|
|
216
|
+
)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""``Runner`` — mirrors ``Runner.run`` / ``Runner.run_streamed`` from the Agents SDK.
|
|
2
|
+
|
|
3
|
+
Both drive one turn against the cloud's beta Agents API:
|
|
4
|
+
``POST /v1/agents/sessions/{id}/events`` (blocking) or
|
|
5
|
+
``POST /v1/agents/sessions/{id}/events/stream`` (SSE).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from typing import Any, AsyncIterator, Optional, Union
|
|
12
|
+
|
|
13
|
+
from .session import Session
|
|
14
|
+
from .transport import async_stream_json, post_json, resolve_client
|
|
15
|
+
from .types import ClientOptions, HistoryInput, HistoryItem, RunResult, StreamedRunResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _to_text(value: HistoryInput) -> str:
|
|
19
|
+
if isinstance(value, str):
|
|
20
|
+
return value
|
|
21
|
+
if isinstance(value, HistoryItem):
|
|
22
|
+
return value.content
|
|
23
|
+
if isinstance(value, dict): # {"role": ..., "content": ...}
|
|
24
|
+
return str(value.get("content", ""))
|
|
25
|
+
parts: list[str] = []
|
|
26
|
+
for item in value or []:
|
|
27
|
+
if isinstance(item, str):
|
|
28
|
+
parts.append(item)
|
|
29
|
+
elif isinstance(item, HistoryItem):
|
|
30
|
+
parts.append(item.content)
|
|
31
|
+
elif isinstance(item, dict):
|
|
32
|
+
parts.append(str(item.get("content", "")))
|
|
33
|
+
return "\n".join(parts)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ensure_session(agent: Any, client: ClientOptions, session: Any) -> Session:
|
|
37
|
+
if isinstance(session, Session):
|
|
38
|
+
return session
|
|
39
|
+
if isinstance(session, str) and session:
|
|
40
|
+
return Session(session, client)
|
|
41
|
+
return Session.create(agent, client)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Runner:
|
|
45
|
+
"""Runs agents against the aria-agent-cloud service."""
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
async def run(
|
|
49
|
+
agent: Any,
|
|
50
|
+
input: HistoryInput,
|
|
51
|
+
*,
|
|
52
|
+
session: Optional[Union[Session, str]] = None,
|
|
53
|
+
client: Optional[ClientOptions] = None,
|
|
54
|
+
) -> RunResult:
|
|
55
|
+
"""Run one turn to completion and return the result.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
result = await Runner.run(agent, "When did the Roman Empire fall?")
|
|
59
|
+
print(result.final_output)
|
|
60
|
+
```
|
|
61
|
+
"""
|
|
62
|
+
merged = client or agent.client
|
|
63
|
+
resolved = resolve_client(merged)
|
|
64
|
+
sess = _ensure_session(agent, merged, session)
|
|
65
|
+
text = _to_text(input)
|
|
66
|
+
|
|
67
|
+
turn = await asyncio.to_thread(
|
|
68
|
+
post_json,
|
|
69
|
+
resolved,
|
|
70
|
+
f"/v1/agents/sessions/{sess.id}/events",
|
|
71
|
+
{"input": text},
|
|
72
|
+
)
|
|
73
|
+
output = turn.get("output") or ""
|
|
74
|
+
sess.record(input, output)
|
|
75
|
+
return RunResult(
|
|
76
|
+
final_output=output,
|
|
77
|
+
history=list(sess.history),
|
|
78
|
+
last_agent=agent,
|
|
79
|
+
session_id=sess.id,
|
|
80
|
+
turn_id=turn.get("id"),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
async def run_streamed(
|
|
85
|
+
agent: Any,
|
|
86
|
+
input: HistoryInput,
|
|
87
|
+
*,
|
|
88
|
+
session: Optional[Union[Session, str]] = None,
|
|
89
|
+
client: Optional[ClientOptions] = None,
|
|
90
|
+
) -> StreamedRunResult:
|
|
91
|
+
"""Run one turn, streaming events (``agent.turn.*`` frames).
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
streamed = await Runner.run_streamed(agent, "Tell me something surprising")
|
|
95
|
+
async for ev in streamed.events:
|
|
96
|
+
if ev["type"] == "agent.turn.output_text.delta":
|
|
97
|
+
print(ev["delta"], end="")
|
|
98
|
+
result = await streamed.completed
|
|
99
|
+
print(result.final_output)
|
|
100
|
+
```
|
|
101
|
+
"""
|
|
102
|
+
merged = client or agent.client
|
|
103
|
+
resolved = resolve_client(merged)
|
|
104
|
+
sess = _ensure_session(agent, merged, session)
|
|
105
|
+
text = _to_text(input)
|
|
106
|
+
|
|
107
|
+
state: dict[str, Any] = {"text": "", "turn_id": None, "error": None}
|
|
108
|
+
buffered: list[dict[str, Any]] = []
|
|
109
|
+
drained = asyncio.Event()
|
|
110
|
+
|
|
111
|
+
def _note(frame: dict[str, Any]) -> None:
|
|
112
|
+
state["turn_id"] = frame.get("turn_id") or state["turn_id"]
|
|
113
|
+
kind = frame.get("type")
|
|
114
|
+
if kind == "agent.turn.output_text.delta":
|
|
115
|
+
state["text"] += str(frame.get("delta") or "")
|
|
116
|
+
elif kind in ("agent.turn.output_text.done", "agent.turn.completed"):
|
|
117
|
+
out = frame.get("text") or (frame.get("turn") or {}).get("output")
|
|
118
|
+
if isinstance(out, str) and out:
|
|
119
|
+
state["text"] = out
|
|
120
|
+
elif kind == "agent.turn.failed":
|
|
121
|
+
state["error"] = (frame.get("error") or {}).get("message") or "run failed"
|
|
122
|
+
|
|
123
|
+
async def _drain() -> None:
|
|
124
|
+
async for frame in async_stream_json(
|
|
125
|
+
resolved,
|
|
126
|
+
f"/v1/agents/sessions/{sess.id}/events/stream",
|
|
127
|
+
{"input": text},
|
|
128
|
+
):
|
|
129
|
+
buffered.append(frame)
|
|
130
|
+
_note(frame)
|
|
131
|
+
drained.set()
|
|
132
|
+
|
|
133
|
+
drain_task = asyncio.ensure_future(_drain())
|
|
134
|
+
|
|
135
|
+
async def events() -> AsyncIterator[dict[str, Any]]:
|
|
136
|
+
await asyncio.shield(drain_task)
|
|
137
|
+
for frame in buffered:
|
|
138
|
+
yield frame
|
|
139
|
+
|
|
140
|
+
async def completed() -> RunResult:
|
|
141
|
+
await asyncio.shield(drain_task)
|
|
142
|
+
if state["error"]:
|
|
143
|
+
raise RuntimeError(state["error"])
|
|
144
|
+
sess.record(input, state["text"])
|
|
145
|
+
return RunResult(
|
|
146
|
+
final_output=state["text"],
|
|
147
|
+
history=list(sess.history),
|
|
148
|
+
last_agent=agent,
|
|
149
|
+
session_id=sess.id,
|
|
150
|
+
turn_id=state["turn_id"],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return StreamedRunResult(events=events(), completed=completed())
|