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.
Files changed (104) hide show
  1. agent_core/__init__.py +525 -0
  2. agent_core/access/__init__.py +6 -0
  3. agent_core/access/context.py +45 -0
  4. agent_core/access/guard.py +40 -0
  5. agent_core/acp/README.md +236 -0
  6. agent_core/acp/__init__.py +20 -0
  7. agent_core/acp/server.py +351 -0
  8. agent_core/acp/types.py +86 -0
  9. agent_core/callbacks/__init__.py +17 -0
  10. agent_core/callbacks/manager.py +105 -0
  11. agent_core/checkpoint/__init__.py +19 -0
  12. agent_core/checkpoint/naming.py +93 -0
  13. agent_core/checkpoint/store.py +186 -0
  14. agent_core/checkpoint/travel.py +448 -0
  15. agent_core/compaction/__init__.py +57 -0
  16. agent_core/compaction/middleware.py +428 -0
  17. agent_core/compaction/prompts.py +32 -0
  18. agent_core/compaction/pruner.py +121 -0
  19. agent_core/compaction/region.py +145 -0
  20. agent_core/compaction/spill.py +81 -0
  21. agent_core/compaction/spill_tool.py +42 -0
  22. agent_core/compaction/summarizer.py +95 -0
  23. agent_core/compaction/token_limit.py +74 -0
  24. agent_core/compaction/types.py +93 -0
  25. agent_core/docs.py +15 -0
  26. agent_core/documents/__init__.py +23 -0
  27. agent_core/documents/loaders.py +79 -0
  28. agent_core/documents/splitters.py +94 -0
  29. agent_core/documents/types.py +197 -0
  30. agent_core/errors.py +81 -0
  31. agent_core/guardrails/__init__.py +19 -0
  32. agent_core/guardrails/core.py +186 -0
  33. agent_core/hitl/__init__.py +15 -0
  34. agent_core/hitl/core.py +517 -0
  35. agent_core/mcp/__init__.py +14 -0
  36. agent_core/mcp/client.py +212 -0
  37. agent_core/mcp/errors.py +26 -0
  38. agent_core/middleware/__init__.py +26 -0
  39. agent_core/middleware/base.py +283 -0
  40. agent_core/model/__init__.py +73 -0
  41. agent_core/model/catalog.py +73 -0
  42. agent_core/model/core.py +179 -0
  43. agent_core/model/governance.py +317 -0
  44. agent_core/model/providers/__init__.py +43 -0
  45. agent_core/model/providers/anthropic.py +294 -0
  46. agent_core/model/providers/common.py +28 -0
  47. agent_core/model/providers/factory.py +107 -0
  48. agent_core/model/providers/gemini.py +273 -0
  49. agent_core/model/providers/ollama.py +80 -0
  50. agent_core/model/providers/openai_compatible.py +526 -0
  51. agent_core/model/structured.py +200 -0
  52. agent_core/observability/__init__.py +21 -0
  53. agent_core/observability/core.py +311 -0
  54. agent_core/ports/__init__.py +31 -0
  55. agent_core/ports/memory.py +130 -0
  56. agent_core/ports/model_selection.py +64 -0
  57. agent_core/ports/storage.py +114 -0
  58. agent_core/prompts/__init__.py +19 -0
  59. agent_core/prompts/registry.py +236 -0
  60. agent_core/prompts/templates.py +166 -0
  61. agent_core/protocol/__init__.py +31 -0
  62. agent_core/protocol/capabilities.py +49 -0
  63. agent_core/protocol/messages.py +100 -0
  64. agent_core/protocol/runtime.py +293 -0
  65. agent_core/py.typed +1 -0
  66. agent_core/retrieval/__init__.py +40 -0
  67. agent_core/retrieval/_common.py +121 -0
  68. agent_core/retrieval/chroma.py +269 -0
  69. agent_core/retrieval/core.py +63 -0
  70. agent_core/retrieval/factory.py +72 -0
  71. agent_core/retrieval/memory.py +232 -0
  72. agent_core/retrieval/pgvector.py +239 -0
  73. agent_core/retrieval/types.py +95 -0
  74. agent_core/runtime/__init__.py +28 -0
  75. agent_core/runtime/compat.py +98 -0
  76. agent_core/runtime/react.py +888 -0
  77. agent_core/runtime/service.py +557 -0
  78. agent_core/runtime/store.py +55 -0
  79. agent_core/serialization/__init__.py +21 -0
  80. agent_core/serialization/registry.py +318 -0
  81. agent_core/skills/__init__.py +24 -0
  82. agent_core/skills/errors.py +27 -0
  83. agent_core/skills/loader.py +258 -0
  84. agent_core/skills/registry.py +205 -0
  85. agent_core/skills/types.py +56 -0
  86. agent_core/storage/__init__.py +107 -0
  87. agent_core/storage/_sqlite_utils.py +92 -0
  88. agent_core/storage/context.py +382 -0
  89. agent_core/storage/lease.py +435 -0
  90. agent_core/storage/memory.py +604 -0
  91. agent_core/storage/model_selection.py +360 -0
  92. agent_core/storage/runtime.py +899 -0
  93. agent_core/storage/session.py +904 -0
  94. agent_core/storage/workflow.py +411 -0
  95. agent_core/tools/__init__.py +5 -0
  96. agent_core/tools/executor.py +306 -0
  97. agent_core/workflow/__init__.py +25 -0
  98. agent_core/workflow/durable.py +175 -0
  99. agent_core/workflow/state_machine.py +427 -0
  100. zero_agent_core-0.2.0.dist-info/METADATA +237 -0
  101. zero_agent_core-0.2.0.dist-info/RECORD +104 -0
  102. zero_agent_core-0.2.0.dist-info/WHEEL +4 -0
  103. zero_agent_core-0.2.0.dist-info/entry_points.txt +3 -0
  104. 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
+ ]