aury-agent 0.0.4__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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Code backend for code execution.
|
|
2
|
+
|
|
3
|
+
Supports executing code in various languages.
|
|
4
|
+
No default implementation - user must provide one.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Literal, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CodeResult:
|
|
14
|
+
"""Result of code execution."""
|
|
15
|
+
output: str
|
|
16
|
+
error: str = ""
|
|
17
|
+
exit_code: int = 0
|
|
18
|
+
language: str = "python"
|
|
19
|
+
duration_ms: int = 0
|
|
20
|
+
|
|
21
|
+
# Optional: execution artifacts (images, files, etc.)
|
|
22
|
+
artifacts: list[dict[str, Any]] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def success(self) -> bool:
|
|
26
|
+
return self.exit_code == 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@runtime_checkable
|
|
30
|
+
class CodeBackend(Protocol):
|
|
31
|
+
"""Protocol for code execution.
|
|
32
|
+
|
|
33
|
+
No default implementation provided.
|
|
34
|
+
Users can implement using:
|
|
35
|
+
- Local subprocess execution
|
|
36
|
+
- E2B code interpreter
|
|
37
|
+
- Jupyter kernel
|
|
38
|
+
- etc.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def supported_languages(self) -> list[str]:
|
|
43
|
+
"""List of supported languages."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
async def execute(
|
|
47
|
+
self,
|
|
48
|
+
code: str,
|
|
49
|
+
language: str = "python",
|
|
50
|
+
timeout: int = 120,
|
|
51
|
+
) -> CodeResult:
|
|
52
|
+
"""Execute code.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
code: Code to execute
|
|
56
|
+
language: Programming language
|
|
57
|
+
timeout: Timeout in seconds
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
CodeResult with output, error, exit_code
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__all__ = ["CodeBackend", "CodeResult"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""File backend for file system operations.
|
|
2
|
+
|
|
3
|
+
Supports different file systems:
|
|
4
|
+
- Local file system
|
|
5
|
+
- Sandbox file system
|
|
6
|
+
- S3/Cloud storage
|
|
7
|
+
"""
|
|
8
|
+
from .types import FileBackend
|
|
9
|
+
from .local import LocalFileBackend
|
|
10
|
+
|
|
11
|
+
__all__ = ["FileBackend", "LocalFileBackend"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Local file system backend."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import aiofiles
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LocalFileBackend:
|
|
10
|
+
"""Local file system backend."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, base_path: str | None = None):
|
|
13
|
+
"""Initialize with optional base path for relative paths."""
|
|
14
|
+
self.base_path = Path(base_path) if base_path else None
|
|
15
|
+
|
|
16
|
+
def _resolve(self, path: str) -> Path:
|
|
17
|
+
"""Resolve path (handle relative paths if base_path set)."""
|
|
18
|
+
p = Path(path)
|
|
19
|
+
if not p.is_absolute() and self.base_path:
|
|
20
|
+
return self.base_path / p
|
|
21
|
+
return p
|
|
22
|
+
|
|
23
|
+
async def read(self, path: str) -> str:
|
|
24
|
+
"""Read file content."""
|
|
25
|
+
async with aiofiles.open(self._resolve(path), "r", encoding="utf-8") as f:
|
|
26
|
+
return await f.read()
|
|
27
|
+
|
|
28
|
+
async def write(self, path: str, content: str) -> None:
|
|
29
|
+
"""Write content to file."""
|
|
30
|
+
p = self._resolve(path)
|
|
31
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
async with aiofiles.open(p, "w", encoding="utf-8") as f:
|
|
33
|
+
await f.write(content)
|
|
34
|
+
|
|
35
|
+
async def append(self, path: str, content: str) -> None:
|
|
36
|
+
"""Append content to file."""
|
|
37
|
+
p = self._resolve(path)
|
|
38
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
async with aiofiles.open(p, "a", encoding="utf-8") as f:
|
|
40
|
+
await f.write(content)
|
|
41
|
+
|
|
42
|
+
async def delete(self, path: str) -> bool:
|
|
43
|
+
"""Delete file."""
|
|
44
|
+
p = self._resolve(path)
|
|
45
|
+
if p.exists():
|
|
46
|
+
p.unlink()
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
async def exists(self, path: str) -> bool:
|
|
51
|
+
"""Check if file exists."""
|
|
52
|
+
return self._resolve(path).exists()
|
|
53
|
+
|
|
54
|
+
async def list(self, path: str, pattern: str = "*") -> list[str]:
|
|
55
|
+
"""List files matching pattern."""
|
|
56
|
+
p = self._resolve(path)
|
|
57
|
+
if not p.exists():
|
|
58
|
+
return []
|
|
59
|
+
return [str(f) for f in p.glob(pattern) if f.is_file()]
|
|
60
|
+
|
|
61
|
+
async def mkdir(self, path: str, parents: bool = True) -> None:
|
|
62
|
+
"""Create directory."""
|
|
63
|
+
self._resolve(path).mkdir(parents=parents, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
__all__ = ["LocalFileBackend"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""File backend types and protocols."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@runtime_checkable
|
|
8
|
+
class FileBackend(Protocol):
|
|
9
|
+
"""Protocol for file system operations."""
|
|
10
|
+
|
|
11
|
+
async def read(self, path: str) -> str:
|
|
12
|
+
"""Read file content."""
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
async def write(self, path: str, content: str) -> None:
|
|
16
|
+
"""Write content to file."""
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
async def append(self, path: str, content: str) -> None:
|
|
20
|
+
"""Append content to file."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
async def delete(self, path: str) -> bool:
|
|
24
|
+
"""Delete file. Returns True if deleted."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
async def exists(self, path: str) -> bool:
|
|
28
|
+
"""Check if file exists."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
async def list(self, path: str, pattern: str = "*") -> list[str]:
|
|
32
|
+
"""List files in directory matching pattern."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
async def mkdir(self, path: str, parents: bool = True) -> None:
|
|
36
|
+
"""Create directory."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["FileBackend"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""In-memory invocation backend implementation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InMemoryInvocationBackend:
|
|
9
|
+
"""In-memory implementation of InvocationBackend.
|
|
10
|
+
|
|
11
|
+
Suitable for testing and simple single-process use cases.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._invocations: dict[str, dict[str, Any]] = {}
|
|
16
|
+
self._session_invocations: dict[str, list[str]] = {}
|
|
17
|
+
|
|
18
|
+
async def create(
|
|
19
|
+
self,
|
|
20
|
+
id: str,
|
|
21
|
+
session_id: str,
|
|
22
|
+
data: dict[str, Any],
|
|
23
|
+
agent_id: str | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Create a new invocation."""
|
|
26
|
+
invocation_data = {
|
|
27
|
+
"id": id,
|
|
28
|
+
"session_id": session_id,
|
|
29
|
+
"agent_id": agent_id,
|
|
30
|
+
"created_at": datetime.now().isoformat(),
|
|
31
|
+
"updated_at": datetime.now().isoformat(),
|
|
32
|
+
**data,
|
|
33
|
+
}
|
|
34
|
+
self._invocations[id] = invocation_data
|
|
35
|
+
|
|
36
|
+
if session_id not in self._session_invocations:
|
|
37
|
+
self._session_invocations[session_id] = []
|
|
38
|
+
self._session_invocations[session_id].append(id)
|
|
39
|
+
|
|
40
|
+
async def get(self, id: str) -> dict[str, Any] | None:
|
|
41
|
+
"""Get invocation by ID."""
|
|
42
|
+
return self._invocations.get(id)
|
|
43
|
+
|
|
44
|
+
async def update(self, id: str, data: dict[str, Any]) -> None:
|
|
45
|
+
"""Update invocation data."""
|
|
46
|
+
if id in self._invocations:
|
|
47
|
+
self._invocations[id].update(data)
|
|
48
|
+
self._invocations[id]["updated_at"] = datetime.now().isoformat()
|
|
49
|
+
|
|
50
|
+
async def delete(self, id: str) -> bool:
|
|
51
|
+
"""Delete an invocation."""
|
|
52
|
+
if id not in self._invocations:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
inv = self._invocations.pop(id)
|
|
56
|
+
session_id = inv.get("session_id")
|
|
57
|
+
if session_id and session_id in self._session_invocations:
|
|
58
|
+
self._session_invocations[session_id] = [
|
|
59
|
+
iid for iid in self._session_invocations[session_id] if iid != id
|
|
60
|
+
]
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
async def list_by_session(
|
|
64
|
+
self,
|
|
65
|
+
session_id: str,
|
|
66
|
+
limit: int = 100,
|
|
67
|
+
offset: int = 0,
|
|
68
|
+
) -> list[dict[str, Any]]:
|
|
69
|
+
"""List invocations for a session."""
|
|
70
|
+
inv_ids = self._session_invocations.get(session_id, [])
|
|
71
|
+
invocations = [self._invocations[iid] for iid in inv_ids if iid in self._invocations]
|
|
72
|
+
invocations.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
|
73
|
+
return invocations[offset:offset + limit]
|
|
74
|
+
|
|
75
|
+
async def get_latest(self, session_id: str) -> dict[str, Any] | None:
|
|
76
|
+
"""Get the latest invocation for a session."""
|
|
77
|
+
invocations = await self.list_by_session(session_id, limit=1)
|
|
78
|
+
return invocations[0] if invocations else None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["InMemoryInvocationBackend"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Invocation backend types and protocols."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@runtime_checkable
|
|
8
|
+
class InvocationBackend(Protocol):
|
|
9
|
+
"""Protocol for invocation management.
|
|
10
|
+
|
|
11
|
+
An invocation represents a single user turn / agent execution cycle.
|
|
12
|
+
Each session can contain multiple invocations.
|
|
13
|
+
|
|
14
|
+
Example usage:
|
|
15
|
+
# Create invocation
|
|
16
|
+
await backend.create(
|
|
17
|
+
id="inv_123",
|
|
18
|
+
session_id="sess_456",
|
|
19
|
+
data={"state": "running", "agent_id": "react_agent"},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Get invocation
|
|
23
|
+
inv = await backend.get("inv_123")
|
|
24
|
+
|
|
25
|
+
# Update state
|
|
26
|
+
await backend.update("inv_123", {"state": "completed"})
|
|
27
|
+
|
|
28
|
+
# List by session
|
|
29
|
+
invocations = await backend.list_by_session("sess_456")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
async def create(
|
|
33
|
+
self,
|
|
34
|
+
id: str,
|
|
35
|
+
session_id: str,
|
|
36
|
+
data: dict[str, Any],
|
|
37
|
+
agent_id: str | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Create a new invocation.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
id: Invocation ID
|
|
43
|
+
session_id: Parent session ID
|
|
44
|
+
data: Invocation data (state, metadata, etc.)
|
|
45
|
+
agent_id: Optional agent ID
|
|
46
|
+
"""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
async def get(self, id: str) -> dict[str, Any] | None:
|
|
50
|
+
"""Get invocation by ID.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
id: Invocation ID
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Invocation data dict or None if not found
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
async def update(self, id: str, data: dict[str, Any]) -> None:
|
|
61
|
+
"""Update invocation data.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
id: Invocation ID
|
|
65
|
+
data: Fields to update (partial update)
|
|
66
|
+
"""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
async def delete(self, id: str) -> bool:
|
|
70
|
+
"""Delete an invocation.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
id: Invocation ID
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if deleted, False if not found
|
|
77
|
+
"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
async def list_by_session(
|
|
81
|
+
self,
|
|
82
|
+
session_id: str,
|
|
83
|
+
limit: int = 100,
|
|
84
|
+
offset: int = 0,
|
|
85
|
+
) -> list[dict[str, Any]]:
|
|
86
|
+
"""List invocations for a session.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
session_id: Session ID to filter by
|
|
90
|
+
limit: Max invocations to return
|
|
91
|
+
offset: Offset for pagination
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of invocation data dicts, ordered by created_at desc
|
|
95
|
+
"""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
async def get_latest(self, session_id: str) -> dict[str, Any] | None:
|
|
99
|
+
"""Get the latest invocation for a session.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
session_id: Session ID
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Latest invocation data or None
|
|
106
|
+
"""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = ["InvocationBackend"]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""In-memory memory backend implementation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InMemoryMemoryBackend:
|
|
11
|
+
"""In-memory implementation of MemoryBackend.
|
|
12
|
+
|
|
13
|
+
Uses simple keyword matching for search.
|
|
14
|
+
Suitable for testing and simple use cases.
|
|
15
|
+
For production, use vector database backends.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._entries: dict[str, dict[str, Any]] = {}
|
|
20
|
+
# Index: session_id -> list of entry_ids
|
|
21
|
+
self._session_entries: dict[str, list[str]] = {}
|
|
22
|
+
# Content hash for deduplication
|
|
23
|
+
self._content_hashes: dict[str, str] = {}
|
|
24
|
+
|
|
25
|
+
def _make_key(self, session_id: str, namespace: str | None) -> str:
|
|
26
|
+
if namespace:
|
|
27
|
+
return f"{session_id}:{namespace}"
|
|
28
|
+
return session_id
|
|
29
|
+
|
|
30
|
+
def _content_hash(self, content: str) -> str:
|
|
31
|
+
return hashlib.sha256(content.encode()).hexdigest()[:16]
|
|
32
|
+
|
|
33
|
+
async def add(
|
|
34
|
+
self,
|
|
35
|
+
session_id: str,
|
|
36
|
+
content: str,
|
|
37
|
+
invocation_id: str | None = None,
|
|
38
|
+
namespace: str | None = None,
|
|
39
|
+
metadata: dict[str, Any] | None = None,
|
|
40
|
+
) -> str:
|
|
41
|
+
"""Add a memory entry with deduplication."""
|
|
42
|
+
content_hash = self._content_hash(content)
|
|
43
|
+
key = self._make_key(session_id, namespace)
|
|
44
|
+
|
|
45
|
+
# Check for duplicate
|
|
46
|
+
hash_key = f"{key}:{content_hash}"
|
|
47
|
+
if hash_key in self._content_hashes:
|
|
48
|
+
return self._content_hashes[hash_key]
|
|
49
|
+
|
|
50
|
+
entry_id = f"mem_{uuid.uuid4().hex[:12]}"
|
|
51
|
+
entry = {
|
|
52
|
+
"id": entry_id,
|
|
53
|
+
"session_id": session_id,
|
|
54
|
+
"content": content,
|
|
55
|
+
"invocation_id": invocation_id,
|
|
56
|
+
"namespace": namespace,
|
|
57
|
+
"metadata": metadata or {},
|
|
58
|
+
"created_at": datetime.now().isoformat(),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
self._entries[entry_id] = entry
|
|
62
|
+
self._content_hashes[hash_key] = entry_id
|
|
63
|
+
|
|
64
|
+
if key not in self._session_entries:
|
|
65
|
+
self._session_entries[key] = []
|
|
66
|
+
self._session_entries[key].append(entry_id)
|
|
67
|
+
|
|
68
|
+
return entry_id
|
|
69
|
+
|
|
70
|
+
async def search(
|
|
71
|
+
self,
|
|
72
|
+
session_id: str,
|
|
73
|
+
query: str,
|
|
74
|
+
namespace: str | None = None,
|
|
75
|
+
limit: int = 10,
|
|
76
|
+
) -> list[dict[str, Any]]:
|
|
77
|
+
"""Simple keyword search."""
|
|
78
|
+
key = self._make_key(session_id, namespace)
|
|
79
|
+
entry_ids = self._session_entries.get(key, [])
|
|
80
|
+
|
|
81
|
+
results = []
|
|
82
|
+
query_lower = query.lower()
|
|
83
|
+
query_words = set(query_lower.split())
|
|
84
|
+
|
|
85
|
+
for entry_id in entry_ids:
|
|
86
|
+
entry = self._entries.get(entry_id)
|
|
87
|
+
if not entry:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
content_lower = entry["content"].lower()
|
|
91
|
+
content_words = set(content_lower.split())
|
|
92
|
+
|
|
93
|
+
# Calculate simple relevance score
|
|
94
|
+
overlap = len(query_words & content_words)
|
|
95
|
+
if overlap > 0:
|
|
96
|
+
score = overlap / len(query_words)
|
|
97
|
+
elif query_lower in content_lower:
|
|
98
|
+
score = 0.5
|
|
99
|
+
else:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
results.append({
|
|
103
|
+
"id": entry["id"],
|
|
104
|
+
"content": entry["content"],
|
|
105
|
+
"score": score,
|
|
106
|
+
"metadata": entry["metadata"],
|
|
107
|
+
"invocation_id": entry.get("invocation_id"),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
results.sort(key=lambda x: x["score"], reverse=True)
|
|
111
|
+
return results[:limit]
|
|
112
|
+
|
|
113
|
+
async def get(self, id: str) -> dict[str, Any] | None:
|
|
114
|
+
"""Get memory by ID."""
|
|
115
|
+
return self._entries.get(id)
|
|
116
|
+
|
|
117
|
+
async def delete(self, id: str) -> bool:
|
|
118
|
+
"""Delete a memory entry."""
|
|
119
|
+
if id not in self._entries:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
entry = self._entries.pop(id)
|
|
123
|
+
session_id = entry["session_id"]
|
|
124
|
+
namespace = entry.get("namespace")
|
|
125
|
+
key = self._make_key(session_id, namespace)
|
|
126
|
+
|
|
127
|
+
if key in self._session_entries:
|
|
128
|
+
self._session_entries[key] = [
|
|
129
|
+
eid for eid in self._session_entries[key] if eid != id
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# Remove from content hash
|
|
133
|
+
content_hash = self._content_hash(entry["content"])
|
|
134
|
+
hash_key = f"{key}:{content_hash}"
|
|
135
|
+
self._content_hashes.pop(hash_key, None)
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
async def delete_by_invocation(
|
|
140
|
+
self,
|
|
141
|
+
session_id: str,
|
|
142
|
+
invocation_id: str,
|
|
143
|
+
namespace: str | None = None,
|
|
144
|
+
) -> int:
|
|
145
|
+
"""Delete memories by invocation."""
|
|
146
|
+
key = self._make_key(session_id, namespace)
|
|
147
|
+
entry_ids = self._session_entries.get(key, [])
|
|
148
|
+
|
|
149
|
+
to_delete = [
|
|
150
|
+
eid for eid in entry_ids
|
|
151
|
+
if self._entries.get(eid, {}).get("invocation_id") == invocation_id
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
for eid in to_delete:
|
|
155
|
+
await self.delete(eid)
|
|
156
|
+
|
|
157
|
+
return len(to_delete)
|
|
158
|
+
|
|
159
|
+
async def list(
|
|
160
|
+
self,
|
|
161
|
+
session_id: str,
|
|
162
|
+
namespace: str | None = None,
|
|
163
|
+
limit: int = 100,
|
|
164
|
+
) -> list[dict[str, Any]]:
|
|
165
|
+
"""List memories for a session."""
|
|
166
|
+
key = self._make_key(session_id, namespace)
|
|
167
|
+
entry_ids = self._session_entries.get(key, [])
|
|
168
|
+
|
|
169
|
+
entries = [
|
|
170
|
+
self._entries[eid]
|
|
171
|
+
for eid in entry_ids
|
|
172
|
+
if eid in self._entries
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
entries.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
|
176
|
+
return entries[:limit]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
__all__ = ["InMemoryMemoryBackend"]
|