zero-agent-core 0.2.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.
- agent_core/__init__.py +525 -0
- agent_core/access/__init__.py +6 -0
- agent_core/access/context.py +45 -0
- agent_core/access/guard.py +40 -0
- agent_core/acp/README.md +236 -0
- agent_core/acp/__init__.py +20 -0
- agent_core/acp/server.py +351 -0
- agent_core/acp/types.py +86 -0
- agent_core/callbacks/__init__.py +17 -0
- agent_core/callbacks/manager.py +105 -0
- agent_core/checkpoint/__init__.py +19 -0
- agent_core/checkpoint/naming.py +93 -0
- agent_core/checkpoint/store.py +186 -0
- agent_core/checkpoint/travel.py +448 -0
- agent_core/compaction/__init__.py +57 -0
- agent_core/compaction/middleware.py +428 -0
- agent_core/compaction/prompts.py +32 -0
- agent_core/compaction/pruner.py +121 -0
- agent_core/compaction/region.py +145 -0
- agent_core/compaction/spill.py +81 -0
- agent_core/compaction/spill_tool.py +42 -0
- agent_core/compaction/summarizer.py +95 -0
- agent_core/compaction/token_limit.py +74 -0
- agent_core/compaction/types.py +93 -0
- agent_core/docs.py +15 -0
- agent_core/documents/__init__.py +23 -0
- agent_core/documents/loaders.py +79 -0
- agent_core/documents/splitters.py +94 -0
- agent_core/documents/types.py +197 -0
- agent_core/errors.py +81 -0
- agent_core/guardrails/__init__.py +19 -0
- agent_core/guardrails/core.py +186 -0
- agent_core/hitl/__init__.py +15 -0
- agent_core/hitl/core.py +517 -0
- agent_core/mcp/__init__.py +14 -0
- agent_core/mcp/client.py +212 -0
- agent_core/mcp/errors.py +26 -0
- agent_core/middleware/__init__.py +26 -0
- agent_core/middleware/base.py +283 -0
- agent_core/model/__init__.py +73 -0
- agent_core/model/catalog.py +73 -0
- agent_core/model/core.py +179 -0
- agent_core/model/governance.py +317 -0
- agent_core/model/providers/__init__.py +43 -0
- agent_core/model/providers/anthropic.py +294 -0
- agent_core/model/providers/common.py +28 -0
- agent_core/model/providers/factory.py +107 -0
- agent_core/model/providers/gemini.py +273 -0
- agent_core/model/providers/ollama.py +80 -0
- agent_core/model/providers/openai_compatible.py +526 -0
- agent_core/model/structured.py +200 -0
- agent_core/observability/__init__.py +21 -0
- agent_core/observability/core.py +311 -0
- agent_core/ports/__init__.py +31 -0
- agent_core/ports/memory.py +130 -0
- agent_core/ports/model_selection.py +64 -0
- agent_core/ports/storage.py +114 -0
- agent_core/prompts/__init__.py +19 -0
- agent_core/prompts/registry.py +236 -0
- agent_core/prompts/templates.py +166 -0
- agent_core/protocol/__init__.py +31 -0
- agent_core/protocol/capabilities.py +49 -0
- agent_core/protocol/messages.py +100 -0
- agent_core/protocol/runtime.py +293 -0
- agent_core/py.typed +1 -0
- agent_core/retrieval/__init__.py +40 -0
- agent_core/retrieval/_common.py +121 -0
- agent_core/retrieval/chroma.py +269 -0
- agent_core/retrieval/core.py +63 -0
- agent_core/retrieval/factory.py +72 -0
- agent_core/retrieval/memory.py +232 -0
- agent_core/retrieval/pgvector.py +239 -0
- agent_core/retrieval/types.py +95 -0
- agent_core/runtime/__init__.py +28 -0
- agent_core/runtime/compat.py +98 -0
- agent_core/runtime/react.py +888 -0
- agent_core/runtime/service.py +557 -0
- agent_core/runtime/store.py +55 -0
- agent_core/serialization/__init__.py +21 -0
- agent_core/serialization/registry.py +318 -0
- agent_core/skills/__init__.py +24 -0
- agent_core/skills/errors.py +27 -0
- agent_core/skills/loader.py +258 -0
- agent_core/skills/registry.py +205 -0
- agent_core/skills/types.py +56 -0
- agent_core/storage/__init__.py +107 -0
- agent_core/storage/_sqlite_utils.py +92 -0
- agent_core/storage/context.py +382 -0
- agent_core/storage/lease.py +435 -0
- agent_core/storage/memory.py +604 -0
- agent_core/storage/model_selection.py +360 -0
- agent_core/storage/runtime.py +899 -0
- agent_core/storage/session.py +904 -0
- agent_core/storage/workflow.py +411 -0
- agent_core/tools/__init__.py +5 -0
- agent_core/tools/executor.py +306 -0
- agent_core/workflow/__init__.py +25 -0
- agent_core/workflow/durable.py +175 -0
- agent_core/workflow/state_machine.py +427 -0
- zero_agent_core-0.2.0.dist-info/METADATA +237 -0
- zero_agent_core-0.2.0.dist-info/RECORD +104 -0
- zero_agent_core-0.2.0.dist-info/WHEEL +4 -0
- zero_agent_core-0.2.0.dist-info/entry_points.txt +3 -0
- zero_agent_core-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""PostgreSQL pgvector 生产向量存储适配器。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from agent_core.access import AccessContext
|
|
11
|
+
from agent_core.retrieval._common import (
|
|
12
|
+
check_record_access,
|
|
13
|
+
decode_record_payload,
|
|
14
|
+
encode_record_payload,
|
|
15
|
+
normalize_record,
|
|
16
|
+
parse_vector,
|
|
17
|
+
validate_vector,
|
|
18
|
+
vector_literal,
|
|
19
|
+
)
|
|
20
|
+
from agent_core.retrieval.core import VectorStore
|
|
21
|
+
from agent_core.retrieval.types import VectorQuery, VectorRecord, VectorSearchResult
|
|
22
|
+
|
|
23
|
+
_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,47}$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PgVectorStore(VectorStore):
|
|
27
|
+
"""使用 asyncpg 和 PostgreSQL pgvector 扩展的生产实现。"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
dsn: str,
|
|
32
|
+
*,
|
|
33
|
+
dimension: int,
|
|
34
|
+
collection_name: str = "agent_core",
|
|
35
|
+
table_name: str = "agent_vector_records",
|
|
36
|
+
require_access: bool = False,
|
|
37
|
+
create_extension: bool = True,
|
|
38
|
+
create_index: bool = True,
|
|
39
|
+
) -> None:
|
|
40
|
+
if dimension <= 0:
|
|
41
|
+
raise ValueError("pgvector dimension 必须大于 0")
|
|
42
|
+
if not _IDENTIFIER.fullmatch(table_name):
|
|
43
|
+
raise ValueError("pgvector table_name 只能包含字母、数字和下划线,且最长 48 字符")
|
|
44
|
+
if not collection_name.strip():
|
|
45
|
+
raise ValueError("pgvector collection_name 不能为空")
|
|
46
|
+
self._dsn = dsn.replace("postgresql+asyncpg://", "postgresql://")
|
|
47
|
+
self._dimension = dimension
|
|
48
|
+
self._collection_name = collection_name
|
|
49
|
+
self._table = f'"{table_name}"'
|
|
50
|
+
self._index = f'"{table_name}_embedding_hnsw"'
|
|
51
|
+
self._require_access = require_access
|
|
52
|
+
self._create_extension = create_extension
|
|
53
|
+
self._create_index = create_index
|
|
54
|
+
self._pool: Any = None
|
|
55
|
+
|
|
56
|
+
async def initialize(self) -> None:
|
|
57
|
+
"""创建连接池、pgvector 扩展、数据表和余弦 HNSW 索引。"""
|
|
58
|
+
try:
|
|
59
|
+
import asyncpg
|
|
60
|
+
except ImportError as exc:
|
|
61
|
+
raise ImportError("PgVectorStore 需要 asyncpg,请安装 zero-agent-core[pgvector]") from exc
|
|
62
|
+
self._pool = await asyncpg.create_pool(self._dsn, min_size=2, max_size=10)
|
|
63
|
+
async with self._pool.acquire() as connection:
|
|
64
|
+
if self._create_extension:
|
|
65
|
+
await connection.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
|
66
|
+
await connection.execute(
|
|
67
|
+
f"""CREATE TABLE IF NOT EXISTS {self._table} (
|
|
68
|
+
collection_name TEXT NOT NULL,
|
|
69
|
+
record_id TEXT NOT NULL,
|
|
70
|
+
namespace TEXT NOT NULL,
|
|
71
|
+
user_id TEXT,
|
|
72
|
+
tenant_id TEXT,
|
|
73
|
+
embedding VECTOR({self._dimension}) NOT NULL,
|
|
74
|
+
payload JSONB NOT NULL,
|
|
75
|
+
search_metadata JSONB NOT NULL DEFAULT '{{}}'::jsonb,
|
|
76
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
77
|
+
PRIMARY KEY (collection_name, record_id)
|
|
78
|
+
)"""
|
|
79
|
+
)
|
|
80
|
+
if self._create_index:
|
|
81
|
+
await connection.execute(
|
|
82
|
+
f"""CREATE INDEX IF NOT EXISTS {self._index}
|
|
83
|
+
ON {self._table} USING hnsw (embedding vector_cosine_ops)"""
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def close(self) -> None:
|
|
87
|
+
if self._pool is not None:
|
|
88
|
+
await self._pool.close()
|
|
89
|
+
self._pool = None
|
|
90
|
+
|
|
91
|
+
async def upsert(
|
|
92
|
+
self,
|
|
93
|
+
records: Sequence[VectorRecord],
|
|
94
|
+
*,
|
|
95
|
+
access: AccessContext | None = None,
|
|
96
|
+
) -> list[str]:
|
|
97
|
+
values = [normalize_record(record, access, require_access=self._require_access) for record in records]
|
|
98
|
+
if not values:
|
|
99
|
+
return []
|
|
100
|
+
for record in values:
|
|
101
|
+
validate_vector(record.vector, dimension=self._dimension)
|
|
102
|
+
self._check_initialized()
|
|
103
|
+
ids = [record.record_id for record in values]
|
|
104
|
+
async with self._pool.acquire() as connection:
|
|
105
|
+
async with connection.transaction():
|
|
106
|
+
if access is not None:
|
|
107
|
+
rows = await connection.fetch(
|
|
108
|
+
f"""SELECT record_id, namespace, user_id, tenant_id, embedding::text AS embedding,
|
|
109
|
+
payload FROM {self._table}
|
|
110
|
+
WHERE collection_name=$1 AND record_id=ANY($2::text[])
|
|
111
|
+
FOR UPDATE""",
|
|
112
|
+
self._collection_name,
|
|
113
|
+
ids,
|
|
114
|
+
)
|
|
115
|
+
for row in rows:
|
|
116
|
+
check_record_access(self._row_to_record(row), access)
|
|
117
|
+
await connection.executemany(
|
|
118
|
+
f"""INSERT INTO {self._table} (
|
|
119
|
+
collection_name, record_id, namespace, user_id, tenant_id,
|
|
120
|
+
embedding, payload, search_metadata
|
|
121
|
+
) VALUES ($1,$2,$3,$4,$5,$6::vector,$7::jsonb,$8::jsonb)
|
|
122
|
+
ON CONFLICT (collection_name, record_id) DO UPDATE SET
|
|
123
|
+
namespace=EXCLUDED.namespace,
|
|
124
|
+
user_id=EXCLUDED.user_id,
|
|
125
|
+
tenant_id=EXCLUDED.tenant_id,
|
|
126
|
+
embedding=EXCLUDED.embedding,
|
|
127
|
+
payload=EXCLUDED.payload,
|
|
128
|
+
search_metadata=EXCLUDED.search_metadata,
|
|
129
|
+
updated_at=NOW()""",
|
|
130
|
+
[
|
|
131
|
+
(
|
|
132
|
+
self._collection_name,
|
|
133
|
+
record.record_id,
|
|
134
|
+
record.namespace,
|
|
135
|
+
record.user_id,
|
|
136
|
+
record.tenant_id,
|
|
137
|
+
vector_literal(record.vector),
|
|
138
|
+
encode_record_payload(record),
|
|
139
|
+
self._json({**record.document.metadata, **record.metadata}),
|
|
140
|
+
)
|
|
141
|
+
for record in values
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
return ids
|
|
145
|
+
|
|
146
|
+
async def delete(
|
|
147
|
+
self,
|
|
148
|
+
record_ids: Sequence[str],
|
|
149
|
+
*,
|
|
150
|
+
access: AccessContext | None = None,
|
|
151
|
+
) -> int:
|
|
152
|
+
self._check_required_access(access)
|
|
153
|
+
if not record_ids:
|
|
154
|
+
return 0
|
|
155
|
+
self._check_initialized()
|
|
156
|
+
async with self._pool.acquire() as connection:
|
|
157
|
+
async with connection.transaction():
|
|
158
|
+
existing = await connection.fetch(
|
|
159
|
+
f"""SELECT record_id, user_id, tenant_id FROM {self._table}
|
|
160
|
+
WHERE collection_name=$1 AND record_id=ANY($2::text[])
|
|
161
|
+
FOR UPDATE""",
|
|
162
|
+
self._collection_name,
|
|
163
|
+
list(record_ids),
|
|
164
|
+
)
|
|
165
|
+
if access is not None:
|
|
166
|
+
for row in existing:
|
|
167
|
+
if not access.can_access(row["user_id"], row["tenant_id"]):
|
|
168
|
+
raise PermissionError(f"无权访问向量记录:{row['record_id']}")
|
|
169
|
+
rows = await connection.fetch(
|
|
170
|
+
f"""DELETE FROM {self._table}
|
|
171
|
+
WHERE collection_name=$1 AND record_id=ANY($2::text[])
|
|
172
|
+
RETURNING record_id""",
|
|
173
|
+
self._collection_name,
|
|
174
|
+
list(record_ids),
|
|
175
|
+
)
|
|
176
|
+
return len(rows)
|
|
177
|
+
|
|
178
|
+
async def search(self, query: VectorQuery) -> list[VectorSearchResult]:
|
|
179
|
+
self._check_required_access(query.access)
|
|
180
|
+
validate_vector(query.vector, dimension=self._dimension)
|
|
181
|
+
self._check_initialized()
|
|
182
|
+
access = query.access
|
|
183
|
+
has_access = access is not None
|
|
184
|
+
rows = []
|
|
185
|
+
async with self._pool.acquire() as connection:
|
|
186
|
+
rows = await connection.fetch(
|
|
187
|
+
f"""SELECT record_id, namespace, user_id, tenant_id,
|
|
188
|
+
embedding::text AS embedding, payload,
|
|
189
|
+
1 - (embedding <=> $1::vector) AS score
|
|
190
|
+
FROM {self._table}
|
|
191
|
+
WHERE collection_name=$2 AND namespace=$3
|
|
192
|
+
AND ($4::boolean=FALSE OR tenant_id IS NULL OR tenant_id=$5)
|
|
193
|
+
AND ($4::boolean=FALSE OR $6::boolean=TRUE OR user_id IS NULL OR user_id=$7)
|
|
194
|
+
AND search_metadata @> $8::jsonb
|
|
195
|
+
AND ($9::double precision IS NULL OR 1 - (embedding <=> $1::vector) >= $9)
|
|
196
|
+
ORDER BY embedding <=> $1::vector, record_id
|
|
197
|
+
LIMIT $10""",
|
|
198
|
+
vector_literal(query.vector),
|
|
199
|
+
self._collection_name,
|
|
200
|
+
query.namespace,
|
|
201
|
+
has_access,
|
|
202
|
+
access.tenant_id if access else None,
|
|
203
|
+
access.is_admin if access else False,
|
|
204
|
+
access.user_id if access else None,
|
|
205
|
+
self._json(query.metadata_filter),
|
|
206
|
+
query.min_score,
|
|
207
|
+
query.limit,
|
|
208
|
+
)
|
|
209
|
+
return [
|
|
210
|
+
VectorSearchResult(record=self._row_to_record(row), score=float(row["score"])) for row in rows
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
def _row_to_record(self, row: Any) -> VectorRecord:
|
|
214
|
+
return decode_record_payload(
|
|
215
|
+
row["payload"],
|
|
216
|
+
record_id=str(row["record_id"]),
|
|
217
|
+
vector=parse_vector(row["embedding"]),
|
|
218
|
+
namespace=str(row["namespace"]),
|
|
219
|
+
user_id=row["user_id"],
|
|
220
|
+
tenant_id=row["tenant_id"],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _check_initialized(self) -> None:
|
|
224
|
+
if self._pool is None:
|
|
225
|
+
raise RuntimeError("PgVectorStore 未初始化,请先调用 initialize()")
|
|
226
|
+
|
|
227
|
+
def _check_required_access(self, access: AccessContext | None) -> None:
|
|
228
|
+
if self._require_access and access is None:
|
|
229
|
+
raise PermissionError("该 VectorStore 要求提供 AccessContext")
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def _json(value: dict[str, Any]) -> str:
|
|
233
|
+
try:
|
|
234
|
+
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
235
|
+
except (TypeError, ValueError) as exc:
|
|
236
|
+
raise ValueError("向量 metadata 必须可以序列化为 JSON") from exc
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
__all__ = ["PgVectorStore"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""检索层公共值对象。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agent_core.access import AccessContext
|
|
10
|
+
from agent_core.documents import Document
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SearchType(StrEnum):
|
|
14
|
+
"""Core 认识的检索策略;具体存储可以只支持其中一部分。"""
|
|
15
|
+
|
|
16
|
+
SIMILARITY = "similarity"
|
|
17
|
+
KEYWORD = "keyword"
|
|
18
|
+
HYBRID = "hybrid"
|
|
19
|
+
MMR = "mmr"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class RetrievalQuery:
|
|
24
|
+
"""与具体检索后端无关的查询。"""
|
|
25
|
+
|
|
26
|
+
text: str
|
|
27
|
+
limit: int = 4
|
|
28
|
+
namespace: str = "default"
|
|
29
|
+
search_type: SearchType = SearchType.SIMILARITY
|
|
30
|
+
metadata_filter: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
min_score: float | None = None
|
|
32
|
+
access: AccessContext | None = None
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
if self.limit <= 0:
|
|
36
|
+
raise ValueError("limit 必须大于 0")
|
|
37
|
+
if not self.namespace.strip():
|
|
38
|
+
raise ValueError("namespace 不能为空")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class RetrievalResult:
|
|
43
|
+
"""Retriever 返回的文档、相关度和来源信息。"""
|
|
44
|
+
|
|
45
|
+
document: Document
|
|
46
|
+
score: float
|
|
47
|
+
rank: int
|
|
48
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class VectorRecord:
|
|
53
|
+
"""向量和原始文档的存储记录。"""
|
|
54
|
+
|
|
55
|
+
record_id: str
|
|
56
|
+
vector: tuple[float, ...]
|
|
57
|
+
document: Document
|
|
58
|
+
namespace: str = "default"
|
|
59
|
+
user_id: str | None = None
|
|
60
|
+
tenant_id: str | None = None
|
|
61
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
if not self.record_id.strip():
|
|
65
|
+
raise ValueError("record_id 不能为空")
|
|
66
|
+
if not self.vector:
|
|
67
|
+
raise ValueError("vector 不能为空")
|
|
68
|
+
if not self.namespace.strip():
|
|
69
|
+
raise ValueError("namespace 不能为空")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class VectorQuery:
|
|
74
|
+
"""VectorStore 接收的底层向量查询。"""
|
|
75
|
+
|
|
76
|
+
vector: tuple[float, ...]
|
|
77
|
+
limit: int = 4
|
|
78
|
+
namespace: str = "default"
|
|
79
|
+
metadata_filter: dict[str, Any] = field(default_factory=dict)
|
|
80
|
+
min_score: float | None = None
|
|
81
|
+
access: AccessContext | None = None
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
if not self.vector:
|
|
85
|
+
raise ValueError("查询 vector 不能为空")
|
|
86
|
+
if self.limit <= 0:
|
|
87
|
+
raise ValueError("limit 必须大于 0")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class VectorSearchResult:
|
|
92
|
+
"""VectorStore 的底层检索结果。"""
|
|
93
|
+
|
|
94
|
+
record: VectorRecord
|
|
95
|
+
score: float
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Agent 运行循环。"""
|
|
2
|
+
|
|
3
|
+
from agent_core.runtime.compat import (
|
|
4
|
+
RunRecord,
|
|
5
|
+
ThreadRecord,
|
|
6
|
+
content_text,
|
|
7
|
+
extract_prompt,
|
|
8
|
+
message_json,
|
|
9
|
+
now_iso,
|
|
10
|
+
)
|
|
11
|
+
from agent_core.runtime.react import ReActAgent
|
|
12
|
+
from agent_core.runtime.service import AgentRuntime, MemoryEventSink, RunExecutor, RuntimeRun
|
|
13
|
+
from agent_core.runtime.store import MemoryRunStore
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AgentRuntime",
|
|
17
|
+
"MemoryEventSink",
|
|
18
|
+
"MemoryRunStore",
|
|
19
|
+
"ReActAgent",
|
|
20
|
+
"RunExecutor",
|
|
21
|
+
"RunRecord",
|
|
22
|
+
"RuntimeRun",
|
|
23
|
+
"ThreadRecord",
|
|
24
|
+
"content_text",
|
|
25
|
+
"extract_prompt",
|
|
26
|
+
"message_json",
|
|
27
|
+
"now_iso",
|
|
28
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""面向 HTTP/前端适配器的通用运行时值对象和消息解析工具。
|
|
2
|
+
|
|
3
|
+
这里不包含 Web 框架或业务编排,只处理跨 Agent 都相同的 thread/run
|
|
4
|
+
记录和输入消息解析。具体 API 协议仍由应用层决定。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from agent_core.protocol.messages import Message
|
|
15
|
+
from agent_core.protocol.runtime import RunContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def now_iso() -> str:
|
|
19
|
+
"""返回 UTC ISO 时间。"""
|
|
20
|
+
return datetime.now(UTC).isoformat()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def message_json(message: Message) -> dict[str, Any]:
|
|
24
|
+
"""把 Core 消息转换为前端可消费的 OpenAI 兼容字典。"""
|
|
25
|
+
return message.to_openai_dict()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ThreadRecord:
|
|
30
|
+
"""通用 thread 运行摘要。"""
|
|
31
|
+
|
|
32
|
+
thread_id: str
|
|
33
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
34
|
+
created_at: str = field(default_factory=now_iso)
|
|
35
|
+
updated_at: str = field(default_factory=now_iso)
|
|
36
|
+
status: str = "idle"
|
|
37
|
+
interrupt: dict[str, Any] | None = None
|
|
38
|
+
pending_prompt: str | None = None
|
|
39
|
+
user_id: str = "anonymous"
|
|
40
|
+
tenant_id: str = "default"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class RunRecord:
|
|
45
|
+
"""通用后台运行句柄。"""
|
|
46
|
+
|
|
47
|
+
run_id: str
|
|
48
|
+
thread_id: str
|
|
49
|
+
task: asyncio.Task[dict[str, Any]]
|
|
50
|
+
context: RunContext
|
|
51
|
+
status: str = "running"
|
|
52
|
+
result: dict[str, Any] | None = None
|
|
53
|
+
error: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def content_text(content: Any) -> str:
|
|
57
|
+
"""兼容字符串消息和常见文本块数组。"""
|
|
58
|
+
if isinstance(content, str):
|
|
59
|
+
return content.strip()
|
|
60
|
+
if not isinstance(content, list):
|
|
61
|
+
return ""
|
|
62
|
+
parts: list[str] = []
|
|
63
|
+
for block in content:
|
|
64
|
+
if isinstance(block, str):
|
|
65
|
+
parts.append(block)
|
|
66
|
+
elif isinstance(block, dict):
|
|
67
|
+
text = block.get("text", block.get("content"))
|
|
68
|
+
if isinstance(text, str):
|
|
69
|
+
parts.append(text)
|
|
70
|
+
return "\n".join(parts).strip()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def extract_prompt(value: Any) -> str:
|
|
74
|
+
"""从 thread/run 输入对象中提取最后一条用户消息。"""
|
|
75
|
+
if not isinstance(value, dict):
|
|
76
|
+
return ""
|
|
77
|
+
messages = value.get("messages")
|
|
78
|
+
if not isinstance(messages, list):
|
|
79
|
+
return ""
|
|
80
|
+
for item in reversed(messages):
|
|
81
|
+
if not isinstance(item, dict):
|
|
82
|
+
continue
|
|
83
|
+
if item.get("role") not in {"user", "human"}:
|
|
84
|
+
continue
|
|
85
|
+
text = content_text(item.get("content"))
|
|
86
|
+
if text:
|
|
87
|
+
return text
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"RunRecord",
|
|
93
|
+
"ThreadRecord",
|
|
94
|
+
"content_text",
|
|
95
|
+
"extract_prompt",
|
|
96
|
+
"message_json",
|
|
97
|
+
"now_iso",
|
|
98
|
+
]
|