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,350 @@
|
|
|
1
|
+
"""Message data structure for LLM communication.
|
|
2
|
+
|
|
3
|
+
Message is the AI layer - separate from Block (UI layer).
|
|
4
|
+
Messages are stored independently and linked to Blocks via invocation_id.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from .session import generate_id
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .artifact import ArtifactRef
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MessageRole(Enum):
|
|
20
|
+
"""Message role."""
|
|
21
|
+
SYSTEM = "system"
|
|
22
|
+
USER = "user"
|
|
23
|
+
ASSISTANT = "assistant"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class MessageContent:
|
|
28
|
+
"""Content block within a Message.
|
|
29
|
+
|
|
30
|
+
Unlike Block (UI layer), MessageContent is purely for LLM communication.
|
|
31
|
+
"""
|
|
32
|
+
type: str # text, image, tool_use, tool_result
|
|
33
|
+
|
|
34
|
+
# text
|
|
35
|
+
text: str | None = None
|
|
36
|
+
|
|
37
|
+
# image
|
|
38
|
+
image_url: str | None = None
|
|
39
|
+
image_base64: str | None = None
|
|
40
|
+
image_media_type: str | None = None
|
|
41
|
+
|
|
42
|
+
# tool_use
|
|
43
|
+
tool_call_id: str | None = None
|
|
44
|
+
tool_name: str | None = None
|
|
45
|
+
tool_arguments: dict[str, Any] | None = None
|
|
46
|
+
|
|
47
|
+
# tool_result
|
|
48
|
+
tool_use_id: str | None = None
|
|
49
|
+
tool_output: str | None = None
|
|
50
|
+
is_error: bool = False
|
|
51
|
+
|
|
52
|
+
def to_llm_format(self) -> dict[str, Any]:
|
|
53
|
+
"""Convert to LLM API format."""
|
|
54
|
+
if self.type == "text":
|
|
55
|
+
return {"type": "text", "text": self.text or ""}
|
|
56
|
+
|
|
57
|
+
elif self.type == "image":
|
|
58
|
+
if self.image_url:
|
|
59
|
+
return {
|
|
60
|
+
"type": "image",
|
|
61
|
+
"source": {"type": "url", "url": self.image_url},
|
|
62
|
+
}
|
|
63
|
+
elif self.image_base64:
|
|
64
|
+
return {
|
|
65
|
+
"type": "image",
|
|
66
|
+
"source": {
|
|
67
|
+
"type": "base64",
|
|
68
|
+
"media_type": self.image_media_type or "image/png",
|
|
69
|
+
"data": self.image_base64,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
elif self.type == "tool_use":
|
|
74
|
+
return {
|
|
75
|
+
"type": "tool_use",
|
|
76
|
+
"id": self.tool_call_id or "",
|
|
77
|
+
"name": self.tool_name or "",
|
|
78
|
+
"input": self.tool_arguments or {},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
elif self.type == "tool_result":
|
|
82
|
+
return {
|
|
83
|
+
"type": "tool_result",
|
|
84
|
+
"tool_use_id": self.tool_use_id or "",
|
|
85
|
+
"content": self.tool_output or "",
|
|
86
|
+
"is_error": self.is_error,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {"type": self.type}
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> dict[str, Any]:
|
|
92
|
+
"""Convert to dictionary for serialization."""
|
|
93
|
+
data: dict[str, Any] = {"type": self.type}
|
|
94
|
+
|
|
95
|
+
if self.text is not None:
|
|
96
|
+
data["text"] = self.text
|
|
97
|
+
if self.image_url is not None:
|
|
98
|
+
data["image_url"] = self.image_url
|
|
99
|
+
if self.image_base64 is not None:
|
|
100
|
+
data["image_base64"] = self.image_base64
|
|
101
|
+
if self.image_media_type is not None:
|
|
102
|
+
data["image_media_type"] = self.image_media_type
|
|
103
|
+
if self.tool_call_id is not None:
|
|
104
|
+
data["tool_call_id"] = self.tool_call_id
|
|
105
|
+
if self.tool_name is not None:
|
|
106
|
+
data["tool_name"] = self.tool_name
|
|
107
|
+
if self.tool_arguments is not None:
|
|
108
|
+
data["tool_arguments"] = self.tool_arguments
|
|
109
|
+
if self.tool_use_id is not None:
|
|
110
|
+
data["tool_use_id"] = self.tool_use_id
|
|
111
|
+
if self.tool_output is not None:
|
|
112
|
+
data["tool_output"] = self.tool_output
|
|
113
|
+
if self.is_error:
|
|
114
|
+
data["is_error"] = self.is_error
|
|
115
|
+
|
|
116
|
+
return data
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_dict(cls, data: dict[str, Any]) -> "MessageContent":
|
|
120
|
+
"""Create from dictionary."""
|
|
121
|
+
return cls(
|
|
122
|
+
type=data["type"],
|
|
123
|
+
text=data.get("text"),
|
|
124
|
+
image_url=data.get("image_url"),
|
|
125
|
+
image_base64=data.get("image_base64"),
|
|
126
|
+
image_media_type=data.get("image_media_type"),
|
|
127
|
+
tool_call_id=data.get("tool_call_id"),
|
|
128
|
+
tool_name=data.get("tool_name"),
|
|
129
|
+
tool_arguments=data.get("tool_arguments"),
|
|
130
|
+
tool_use_id=data.get("tool_use_id"),
|
|
131
|
+
tool_output=data.get("tool_output"),
|
|
132
|
+
is_error=data.get("is_error", False),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Factory methods
|
|
136
|
+
@classmethod
|
|
137
|
+
def text(cls, content: str) -> "MessageContent":
|
|
138
|
+
"""Create text content."""
|
|
139
|
+
return cls(type="text", text=content)
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def tool_use(cls, call_id: str, name: str, arguments: dict[str, Any]) -> "MessageContent":
|
|
143
|
+
"""Create tool use content."""
|
|
144
|
+
return cls(
|
|
145
|
+
type="tool_use",
|
|
146
|
+
tool_call_id=call_id,
|
|
147
|
+
tool_name=name,
|
|
148
|
+
tool_arguments=arguments,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def tool_result(cls, tool_use_id: str, output: str, is_error: bool = False) -> "MessageContent":
|
|
153
|
+
"""Create tool result content."""
|
|
154
|
+
return cls(
|
|
155
|
+
type="tool_result",
|
|
156
|
+
tool_use_id=tool_use_id,
|
|
157
|
+
tool_output=output,
|
|
158
|
+
is_error=is_error,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class Message:
|
|
164
|
+
"""A message in the conversation (AI layer).
|
|
165
|
+
|
|
166
|
+
Message is separate from Block:
|
|
167
|
+
- Message: AI communication layer (stored for LLM context)
|
|
168
|
+
- Block: UI display layer (stored for frontend rendering)
|
|
169
|
+
|
|
170
|
+
They are linked via invocation_id for revert operations.
|
|
171
|
+
"""
|
|
172
|
+
id: str = field(default_factory=lambda: generate_id("msg"))
|
|
173
|
+
role: MessageRole = MessageRole.USER
|
|
174
|
+
|
|
175
|
+
# Content (multiple formats supported)
|
|
176
|
+
content: list[MessageContent] = field(default_factory=list)
|
|
177
|
+
|
|
178
|
+
# Artifact references (full content in ArtifactStore)
|
|
179
|
+
artifact_refs: list["ArtifactRef"] = field(default_factory=list)
|
|
180
|
+
|
|
181
|
+
# Session context
|
|
182
|
+
session_id: str = ""
|
|
183
|
+
invocation_id: str = ""
|
|
184
|
+
|
|
185
|
+
# Branch for sub-agent isolation
|
|
186
|
+
branch: str | None = None
|
|
187
|
+
|
|
188
|
+
# Timestamps
|
|
189
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
190
|
+
|
|
191
|
+
# Extension
|
|
192
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def text_content(self) -> str:
|
|
196
|
+
"""Get combined text content."""
|
|
197
|
+
parts = []
|
|
198
|
+
for c in self.content:
|
|
199
|
+
if c.type == "text" and c.text:
|
|
200
|
+
parts.append(c.text)
|
|
201
|
+
return "".join(parts)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def tool_calls(self) -> list[MessageContent]:
|
|
205
|
+
"""Get all tool_use content."""
|
|
206
|
+
return [c for c in self.content if c.type == "tool_use"]
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def tool_results(self) -> list[MessageContent]:
|
|
210
|
+
"""Get all tool_result content."""
|
|
211
|
+
return [c for c in self.content if c.type == "tool_result"]
|
|
212
|
+
|
|
213
|
+
def to_llm_format(self) -> dict[str, Any]:
|
|
214
|
+
"""Convert to LLM API format (Anthropic-style).
|
|
215
|
+
|
|
216
|
+
Returns a message dict suitable for LLM API calls.
|
|
217
|
+
Artifact references are converted to text descriptions.
|
|
218
|
+
"""
|
|
219
|
+
content_parts: list[dict[str, Any]] = []
|
|
220
|
+
|
|
221
|
+
# Add regular content
|
|
222
|
+
for c in self.content:
|
|
223
|
+
llm_part = c.to_llm_format()
|
|
224
|
+
if llm_part:
|
|
225
|
+
content_parts.append(llm_part)
|
|
226
|
+
|
|
227
|
+
# Add artifact references as context
|
|
228
|
+
if self.artifact_refs:
|
|
229
|
+
refs_text = "\n\n[Available Artifacts]\n"
|
|
230
|
+
for ref in self.artifact_refs:
|
|
231
|
+
refs_text += f"- [artifact:{ref.artifact_id}] {ref.title or 'Untitled'}: {ref.summary or 'No summary'}\n"
|
|
232
|
+
content_parts.append({"type": "text", "text": refs_text})
|
|
233
|
+
|
|
234
|
+
# Simplify if single text content
|
|
235
|
+
if len(content_parts) == 1 and content_parts[0].get("type") == "text":
|
|
236
|
+
return {
|
|
237
|
+
"role": self.role.value,
|
|
238
|
+
"content": content_parts[0]["text"],
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
"role": self.role.value,
|
|
243
|
+
"content": content_parts,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
def to_dict(self) -> dict[str, Any]:
|
|
247
|
+
"""Convert to dictionary for serialization."""
|
|
248
|
+
return {
|
|
249
|
+
"id": self.id,
|
|
250
|
+
"role": self.role.value,
|
|
251
|
+
"content": [c.to_dict() for c in self.content],
|
|
252
|
+
"artifact_refs": [r.to_dict() for r in self.artifact_refs] if self.artifact_refs else [],
|
|
253
|
+
"session_id": self.session_id,
|
|
254
|
+
"invocation_id": self.invocation_id,
|
|
255
|
+
"branch": self.branch,
|
|
256
|
+
"created_at": self.created_at.isoformat(),
|
|
257
|
+
"metadata": self.metadata,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def from_dict(cls, data: dict[str, Any]) -> "Message":
|
|
262
|
+
"""Create from dictionary."""
|
|
263
|
+
from .artifact import ArtifactRef
|
|
264
|
+
|
|
265
|
+
return cls(
|
|
266
|
+
id=data["id"],
|
|
267
|
+
role=MessageRole(data["role"]),
|
|
268
|
+
content=[MessageContent.from_dict(c) for c in data.get("content", [])],
|
|
269
|
+
artifact_refs=[ArtifactRef.from_dict(r) for r in data.get("artifact_refs", [])],
|
|
270
|
+
session_id=data.get("session_id", ""),
|
|
271
|
+
invocation_id=data.get("invocation_id", ""),
|
|
272
|
+
branch=data.get("branch"),
|
|
273
|
+
created_at=datetime.fromisoformat(data["created_at"]) if "created_at" in data else datetime.now(),
|
|
274
|
+
metadata=data.get("metadata", {}),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Factory methods
|
|
278
|
+
@classmethod
|
|
279
|
+
def user(cls, text: str, session_id: str = "", invocation_id: str = "") -> "Message":
|
|
280
|
+
"""Create a user message."""
|
|
281
|
+
return cls(
|
|
282
|
+
role=MessageRole.USER,
|
|
283
|
+
content=[MessageContent.text(text)],
|
|
284
|
+
session_id=session_id,
|
|
285
|
+
invocation_id=invocation_id,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def assistant(cls, text: str, session_id: str = "", invocation_id: str = "") -> "Message":
|
|
290
|
+
"""Create an assistant message."""
|
|
291
|
+
return cls(
|
|
292
|
+
role=MessageRole.ASSISTANT,
|
|
293
|
+
content=[MessageContent.text(text)],
|
|
294
|
+
session_id=session_id,
|
|
295
|
+
invocation_id=invocation_id,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
def system(cls, text: str) -> "Message":
|
|
300
|
+
"""Create a system message."""
|
|
301
|
+
return cls(
|
|
302
|
+
role=MessageRole.SYSTEM,
|
|
303
|
+
content=[MessageContent.text(text)],
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass
|
|
308
|
+
class PromptInput:
|
|
309
|
+
"""User input for agent invocation.
|
|
310
|
+
|
|
311
|
+
Attributes:
|
|
312
|
+
text: User message text
|
|
313
|
+
runtime_tools: Tools to add for this run only
|
|
314
|
+
vars: Runtime variables (accessible by Managers via ctx.input.vars)
|
|
315
|
+
attachments: Image/file attachments
|
|
316
|
+
metadata: Additional metadata
|
|
317
|
+
"""
|
|
318
|
+
text: str
|
|
319
|
+
runtime_tools: list[Any] = field(default_factory=list) # list[BaseTool]
|
|
320
|
+
vars: dict[str, Any] = field(default_factory=dict)
|
|
321
|
+
attachments: list[dict[str, Any]] | None = None
|
|
322
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
323
|
+
|
|
324
|
+
def to_message(self, session_id: str = "", invocation_id: str = "") -> Message:
|
|
325
|
+
"""Convert to a Message."""
|
|
326
|
+
content = [MessageContent.text(self.text)]
|
|
327
|
+
|
|
328
|
+
# Handle attachments (images, etc.)
|
|
329
|
+
if self.attachments:
|
|
330
|
+
for att in self.attachments:
|
|
331
|
+
if att.get("type") == "image":
|
|
332
|
+
if "url" in att:
|
|
333
|
+
content.append(MessageContent(
|
|
334
|
+
type="image",
|
|
335
|
+
image_url=att["url"],
|
|
336
|
+
))
|
|
337
|
+
elif "base64" in att:
|
|
338
|
+
content.append(MessageContent(
|
|
339
|
+
type="image",
|
|
340
|
+
image_base64=att["base64"],
|
|
341
|
+
image_media_type=att.get("media_type", "image/png"),
|
|
342
|
+
))
|
|
343
|
+
|
|
344
|
+
return Message(
|
|
345
|
+
role=MessageRole.USER,
|
|
346
|
+
content=content,
|
|
347
|
+
session_id=session_id,
|
|
348
|
+
invocation_id=invocation_id,
|
|
349
|
+
metadata=self.metadata,
|
|
350
|
+
)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Recall data structures for session memory.
|
|
2
|
+
|
|
3
|
+
Recalls are key points within a session:
|
|
4
|
+
- Extracted from conversation (user info, preferences, decisions)
|
|
5
|
+
- Used for quick context retrieval
|
|
6
|
+
- Support revert (linked to invocation_id)
|
|
7
|
+
- Support SubAgent isolation (branch field)
|
|
8
|
+
|
|
9
|
+
Unlike Knowledge (cross-session, user-defined), Recalls are:
|
|
10
|
+
- Session-scoped
|
|
11
|
+
- Framework-managed
|
|
12
|
+
- Automatically linked to invocations for revert
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from .session import generate_id
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Recall:
|
|
25
|
+
"""A recall point (session memory).
|
|
26
|
+
|
|
27
|
+
Captures important information from the conversation
|
|
28
|
+
for quick retrieval and context building.
|
|
29
|
+
"""
|
|
30
|
+
id: str = field(default_factory=lambda: generate_id("rcl"))
|
|
31
|
+
|
|
32
|
+
# Content
|
|
33
|
+
content: str = ""
|
|
34
|
+
importance: float = 0.5 # 0.0 - 1.0
|
|
35
|
+
|
|
36
|
+
# Classification
|
|
37
|
+
category: str | None = None # user_info, preference, task, decision, fact...
|
|
38
|
+
tags: list[str] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
# Session context (for revert)
|
|
41
|
+
session_id: str = ""
|
|
42
|
+
invocation_id: str = "" # Source invocation (used for revert)
|
|
43
|
+
|
|
44
|
+
# SubAgent isolation
|
|
45
|
+
branch: str | None = None
|
|
46
|
+
|
|
47
|
+
# Timestamps
|
|
48
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
49
|
+
expires_at: datetime | None = None # Optional TTL
|
|
50
|
+
|
|
51
|
+
# Extension
|
|
52
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict[str, Any]:
|
|
55
|
+
"""Convert to dictionary for serialization."""
|
|
56
|
+
return {
|
|
57
|
+
"id": self.id,
|
|
58
|
+
"content": self.content,
|
|
59
|
+
"importance": self.importance,
|
|
60
|
+
"category": self.category,
|
|
61
|
+
"tags": self.tags,
|
|
62
|
+
"session_id": self.session_id,
|
|
63
|
+
"invocation_id": self.invocation_id,
|
|
64
|
+
"branch": self.branch,
|
|
65
|
+
"created_at": self.created_at.isoformat(),
|
|
66
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
67
|
+
"metadata": self.metadata,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_dict(cls, data: dict[str, Any]) -> "Recall":
|
|
72
|
+
"""Create from dictionary."""
|
|
73
|
+
return cls(
|
|
74
|
+
id=data["id"],
|
|
75
|
+
content=data.get("content", ""),
|
|
76
|
+
importance=data.get("importance", 0.5),
|
|
77
|
+
category=data.get("category"),
|
|
78
|
+
tags=data.get("tags", []),
|
|
79
|
+
session_id=data.get("session_id", ""),
|
|
80
|
+
invocation_id=data.get("invocation_id", ""),
|
|
81
|
+
branch=data.get("branch"),
|
|
82
|
+
created_at=datetime.fromisoformat(data["created_at"]) if "created_at" in data else datetime.now(),
|
|
83
|
+
expires_at=datetime.fromisoformat(data["expires_at"]) if data.get("expires_at") else None,
|
|
84
|
+
metadata=data.get("metadata", {}),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class Summary:
|
|
90
|
+
"""Session summary (compressed history).
|
|
91
|
+
|
|
92
|
+
Reduces token consumption by summarizing old messages.
|
|
93
|
+
"""
|
|
94
|
+
id: str = field(default_factory=lambda: generate_id("sum"))
|
|
95
|
+
|
|
96
|
+
# Content
|
|
97
|
+
content: str = ""
|
|
98
|
+
|
|
99
|
+
# Coverage
|
|
100
|
+
session_id: str = ""
|
|
101
|
+
covers_until_invocation: str = "" # Summary covers up to this invocation
|
|
102
|
+
compressed_message_count: int = 0
|
|
103
|
+
|
|
104
|
+
# Token stats
|
|
105
|
+
token_count: int = 0
|
|
106
|
+
|
|
107
|
+
# Timestamps
|
|
108
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
109
|
+
updated_at: datetime | None = None
|
|
110
|
+
|
|
111
|
+
# Extension
|
|
112
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
113
|
+
|
|
114
|
+
def to_dict(self) -> dict[str, Any]:
|
|
115
|
+
"""Convert to dictionary for serialization."""
|
|
116
|
+
return {
|
|
117
|
+
"id": self.id,
|
|
118
|
+
"content": self.content,
|
|
119
|
+
"session_id": self.session_id,
|
|
120
|
+
"covers_until_invocation": self.covers_until_invocation,
|
|
121
|
+
"compressed_message_count": self.compressed_message_count,
|
|
122
|
+
"token_count": self.token_count,
|
|
123
|
+
"created_at": self.created_at.isoformat(),
|
|
124
|
+
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
125
|
+
"metadata": self.metadata,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def from_dict(cls, data: dict[str, Any]) -> "Summary":
|
|
130
|
+
"""Create from dictionary."""
|
|
131
|
+
return cls(
|
|
132
|
+
id=data["id"],
|
|
133
|
+
content=data.get("content", ""),
|
|
134
|
+
session_id=data.get("session_id", ""),
|
|
135
|
+
covers_until_invocation=data.get("covers_until_invocation", ""),
|
|
136
|
+
compressed_message_count=data.get("compressed_message_count", 0),
|
|
137
|
+
token_count=data.get("token_count", 0),
|
|
138
|
+
created_at=datetime.fromisoformat(data["created_at"]) if "created_at" in data else datetime.now(),
|
|
139
|
+
updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else None,
|
|
140
|
+
metadata=data.get("metadata", {}),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
__all__ = ["Recall", "Summary"]
|