framework-agents 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.
- framework_agents/__init__.py +345 -0
- framework_agents/a2a_client.py +360 -0
- framework_agents/a2a_server.py +495 -0
- framework_agents/agent.py +920 -0
- framework_agents/checkpoint.py +231 -0
- framework_agents/config.py +200 -0
- framework_agents/conversation.py +249 -0
- framework_agents/errors.py +47 -0
- framework_agents/evals.py +127 -0
- framework_agents/mcp_client.py +477 -0
- framework_agents/memory.py +103 -0
- framework_agents/memory_compaction.py +63 -0
- framework_agents/middleware.py +1302 -0
- framework_agents/models/__init__.py +100 -0
- framework_agents/models/anthropic.py +339 -0
- framework_agents/models/capabilities.py +45 -0
- framework_agents/models/ollama.py +47 -0
- framework_agents/models/openai_compatible.py +409 -0
- framework_agents/observability.py +84 -0
- framework_agents/ontology_review.py +321 -0
- framework_agents/orchestration.py +169 -0
- framework_agents/pricing.py +63 -0
- framework_agents/prompts.py +132 -0
- framework_agents/rag.py +123 -0
- framework_agents/remote_sparql.py +213 -0
- framework_agents/runtime.py +88 -0
- framework_agents/sandbox.py +327 -0
- framework_agents/semantic.py +1614 -0
- framework_agents/serve.py +436 -0
- framework_agents/sessions.py +249 -0
- framework_agents/skills.py +213 -0
- framework_agents/state.py +89 -0
- framework_agents/streaming.py +74 -0
- framework_agents/swarm.py +318 -0
- framework_agents/tools.py +114 -0
- framework_agents/tracing.py +131 -0
- framework_agents/types.py +262 -0
- framework_agents/workflows.py +130 -0
- framework_agents-0.1.0.dist-info/METADATA +118 -0
- framework_agents-0.1.0.dist-info/RECORD +41 -0
- framework_agents-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Memory: append + window. Parity: src/memory/index.ts (post-fix).
|
|
2
|
+
|
|
3
|
+
Pinned semantics (the TS sweep found these the hard way):
|
|
4
|
+
- save() APPENDS (windows to max); the agent loop does CLEAR-then-SAVE, never
|
|
5
|
+
plain save, or every run duplicates the transcript.
|
|
6
|
+
- FileMemory: corrupted files fail LOUDLY (MEMORY_CORRUPT) instead of wiping
|
|
7
|
+
whatever is still on disk on the next save; missing file = fresh memory.
|
|
8
|
+
- tmp file per flush is unique — concurrent flushes can't race on rename.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import tempfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Protocol
|
|
18
|
+
|
|
19
|
+
from .errors import AgentError
|
|
20
|
+
from .types import ChatMessage, deserialize_messages, serialize_messages
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Memory(Protocol):
|
|
24
|
+
async def load(self) -> list[ChatMessage]: ...
|
|
25
|
+
async def save(self, messages: list[ChatMessage]) -> None: ...
|
|
26
|
+
async def clear(self) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InMemoryMemory:
|
|
30
|
+
def __init__(self, max_messages: int = 50) -> None:
|
|
31
|
+
self._messages: list[ChatMessage] = []
|
|
32
|
+
self.max_messages = max_messages
|
|
33
|
+
|
|
34
|
+
async def load(self) -> list[ChatMessage]:
|
|
35
|
+
return list(self._messages)
|
|
36
|
+
|
|
37
|
+
async def save(self, messages: list[ChatMessage]) -> None:
|
|
38
|
+
self._messages = [*self._messages, *messages][-self.max_messages:]
|
|
39
|
+
|
|
40
|
+
async def clear(self) -> None:
|
|
41
|
+
self._messages = []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FileMemory:
|
|
45
|
+
"""JSON-file backed memory: same windowing as in-memory, atomic writes."""
|
|
46
|
+
|
|
47
|
+
_MAX_UTF16_UNITS = 2048
|
|
48
|
+
|
|
49
|
+
def __init__(self, directory: str, max_messages: int = 50) -> None:
|
|
50
|
+
self.directory = Path(directory)
|
|
51
|
+
self.file = self.directory / "memory.json"
|
|
52
|
+
self.max_messages = max_messages
|
|
53
|
+
self._cache: list[ChatMessage] | None = None
|
|
54
|
+
|
|
55
|
+
async def load(self) -> list[ChatMessage]:
|
|
56
|
+
return list(await self._read_all())
|
|
57
|
+
|
|
58
|
+
async def save(self, messages: list[ChatMessage]) -> None:
|
|
59
|
+
all_messages = [*(await self._read_all()), *messages][-self.max_messages:]
|
|
60
|
+
self._cache = all_messages
|
|
61
|
+
await self._flush()
|
|
62
|
+
|
|
63
|
+
async def clear(self) -> None:
|
|
64
|
+
self._cache = []
|
|
65
|
+
await self._flush()
|
|
66
|
+
|
|
67
|
+
async def _read_all(self) -> list[ChatMessage]:
|
|
68
|
+
if self._cache is not None:
|
|
69
|
+
return self._cache
|
|
70
|
+
try:
|
|
71
|
+
raw = self.file.read_text(encoding="utf-8")
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
self._cache = []
|
|
74
|
+
return self._cache
|
|
75
|
+
except OSError as err:
|
|
76
|
+
self._cache = []
|
|
77
|
+
return self._cache
|
|
78
|
+
try:
|
|
79
|
+
data = json.loads(raw)
|
|
80
|
+
# Valid JSON with the WRONG shape is still corrupt data. TS
|
|
81
|
+
# throws here (deserializeMessages rejects non-arrays); silently
|
|
82
|
+
# treating a dict as "empty" would clobber the file on save.
|
|
83
|
+
if not isinstance(data, list):
|
|
84
|
+
raise ValueError("top-level JSON is not a message array")
|
|
85
|
+
self._cache = deserialize_messages(data)
|
|
86
|
+
except (ValueError, TypeError) as err:
|
|
87
|
+
# Corrupted file must FAIL loudly: returning [] would make the
|
|
88
|
+
# next save overwrite whatever was still on disk.
|
|
89
|
+
raise AgentError("MEMORY_CORRUPT", f"Memory file is not valid JSON for messages ({self.file}): {err}", retryable=False) from err
|
|
90
|
+
return self._cache
|
|
91
|
+
|
|
92
|
+
async def _flush(self) -> None:
|
|
93
|
+
self.directory.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
# Unique tmp per flush: concurrent saves sharing one fixed name race
|
|
95
|
+
# on the rename and can publish a torn or half-written file.
|
|
96
|
+
fd, tmp_path = tempfile.mkstemp(dir=self.directory, prefix="memory.json.", suffix=".tmp")
|
|
97
|
+
try:
|
|
98
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
99
|
+
json.dump(serialize_messages(self._cache or []), fh)
|
|
100
|
+
os.replace(tmp_path, self.file)
|
|
101
|
+
finally:
|
|
102
|
+
if os.path.exists(tmp_path): # replace succeeded -> gone already
|
|
103
|
+
os.unlink(tmp_path)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Compaction: keep long runs inside the context window without losing meaning.
|
|
2
|
+
|
|
3
|
+
Parity: src/memory/compaction.ts.
|
|
4
|
+
|
|
5
|
+
Strategy (P1): oldest messages -> model/pluggable summarizer -> single
|
|
6
|
+
boundary message.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import inspect
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
from .memory import Memory
|
|
15
|
+
from .types import ChatMessage, text_of
|
|
16
|
+
|
|
17
|
+
SummarizeFn = Callable[[list[ChatMessage]], Any] # str or awaitable str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def default_summarize(messages: list[ChatMessage]) -> str:
|
|
21
|
+
"""Extractive stub: first five non-system messages, 200 chars each."""
|
|
22
|
+
head = "\n".join(
|
|
23
|
+
f"[{m.role}] {text_of(m.content)[:200]}"
|
|
24
|
+
for m in [m for m in messages if m.role != "system"][:5]
|
|
25
|
+
)
|
|
26
|
+
return f"Prior context summary ({len(messages)} msgs):\n{head}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CompactedMemory:
|
|
30
|
+
"""Wrap any Memory with auto-compaction on save."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
inner: Memory,
|
|
35
|
+
max_messages: int = 40,
|
|
36
|
+
summarize: SummarizeFn | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.inner = inner
|
|
39
|
+
self.max_messages = max_messages
|
|
40
|
+
self.summarize = summarize
|
|
41
|
+
|
|
42
|
+
async def load(self) -> list[ChatMessage]:
|
|
43
|
+
return await self.inner.load()
|
|
44
|
+
|
|
45
|
+
async def save(self, messages: list[ChatMessage]) -> None:
|
|
46
|
+
await self.inner.save(messages)
|
|
47
|
+
max = self.max_messages
|
|
48
|
+
current = await self.inner.load()
|
|
49
|
+
if len(current) <= max:
|
|
50
|
+
return
|
|
51
|
+
summarize = self.summarize or default_summarize
|
|
52
|
+
# Overflow is replaced by ONE summary message so the stored history
|
|
53
|
+
# shrinks back to exactly `max` (1 summary + max-1 newest).
|
|
54
|
+
overflow = current[: len(current) - max + 1]
|
|
55
|
+
rest = current[len(current) - max + 1 :]
|
|
56
|
+
summary = summarize(overflow)
|
|
57
|
+
if inspect.isawaitable(summary):
|
|
58
|
+
summary = await summary
|
|
59
|
+
await self.inner.clear()
|
|
60
|
+
await self.inner.save([ChatMessage(role="assistant", content=summary), *rest])
|
|
61
|
+
|
|
62
|
+
async def clear(self) -> None:
|
|
63
|
+
await self.inner.clear()
|