python-library-ai-agent 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.
Files changed (62) hide show
  1. ai_agent/__init__.py +66 -0
  2. ai_agent/agent.py +122 -0
  3. ai_agent/app/__init__.py +10 -0
  4. ai_agent/app/_workspace.py +127 -0
  5. ai_agent/app/app.py +321 -0
  6. ai_agent/app/harness_io.py +109 -0
  7. ai_agent/app/output_format.py +77 -0
  8. ai_agent/app/packet.py +39 -0
  9. ai_agent/app/session.py +742 -0
  10. ai_agent/app/session_store.py +85 -0
  11. ai_agent/builtin_tools/__init__.py +18 -0
  12. ai_agent/builtin_tools/current_time.py +39 -0
  13. ai_agent/builtin_tools/pack.py +20 -0
  14. ai_agent/builtin_tools/prefix.py +11 -0
  15. ai_agent/context.py +151 -0
  16. ai_agent/harness/__init__.py +3 -0
  17. ai_agent/harness/current_time.py +25 -0
  18. ai_agent/harness/harness.py +324 -0
  19. ai_agent/harness/process.py +115 -0
  20. ai_agent/harness/prompts.py +38 -0
  21. ai_agent/harness/sandbox.py +139 -0
  22. ai_agent/json_extract.py +70 -0
  23. ai_agent/listener.py +172 -0
  24. ai_agent/llm.py +39 -0
  25. ai_agent/llm_openai.py +117 -0
  26. ai_agent/loop.py +124 -0
  27. ai_agent/mcp_config.py +54 -0
  28. ai_agent/mcp_loader.py +110 -0
  29. ai_agent/memory/__init__.py +9 -0
  30. ai_agent/memory/compression_work.py +71 -0
  31. ai_agent/memory/compressor.py +339 -0
  32. ai_agent/memory/config.py +40 -0
  33. ai_agent/memory/context_builder.py +57 -0
  34. ai_agent/memory/memory_system.py +561 -0
  35. ai_agent/memory/models.py +76 -0
  36. ai_agent/memory/snapshot_merge.py +158 -0
  37. ai_agent/memory/store.py +107 -0
  38. ai_agent/memory/worker.py +227 -0
  39. ai_agent/plan/__init__.py +15 -0
  40. ai_agent/plan/complete.py +64 -0
  41. ai_agent/plan/delivery.py +41 -0
  42. ai_agent/plan/display.py +46 -0
  43. ai_agent/plan/models.py +44 -0
  44. ai_agent/plan/parse.py +39 -0
  45. ai_agent/plan/planner.py +204 -0
  46. ai_agent/plan/runner.py +281 -0
  47. ai_agent/react_tool_turn.py +39 -0
  48. ai_agent/rule/__init__.py +3 -0
  49. ai_agent/rule/rules.py +36 -0
  50. ai_agent/skill/__init__.py +5 -0
  51. ai_agent/skill/builtin_registry.py +56 -0
  52. ai_agent/skill/catalog.py +104 -0
  53. ai_agent/skill/frontmatter.py +83 -0
  54. ai_agent/skill/manager.py +486 -0
  55. ai_agent/skill/models.py +31 -0
  56. ai_agent/skill/roots.py +150 -0
  57. ai_agent/skill/skill_kit.py +80 -0
  58. ai_agent/skill/tool_declarations.py +68 -0
  59. ai_agent/tools.py +123 -0
  60. python_library_ai_agent-0.1.0.dist-info/METADATA +10 -0
  61. python_library_ai_agent-0.1.0.dist-info/RECORD +62 -0
  62. python_library_ai_agent-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,561 @@
1
+ from __future__ import annotations
2
+
3
+
4
+
5
+ import threading
6
+
7
+ from datetime import datetime, timezone
8
+
9
+ from pathlib import Path
10
+
11
+ from typing import Literal
12
+
13
+
14
+
15
+ from openai import AsyncOpenAI
16
+
17
+
18
+
19
+ from ai_agent.context import ChatMessage
20
+
21
+ from ai_agent.llm_openai import OpenAILLM
22
+
23
+ from ai_agent.memory.compressor import (
24
+
25
+ LLMMemoryCompressor,
26
+
27
+ MemoryCompressor,
28
+
29
+ RuleMemoryCompressor,
30
+
31
+ )
32
+
33
+ from ai_agent.memory.config import MemoryConfig
34
+
35
+ from ai_agent.memory.compression_work import CompressionResult, CompressionWork
36
+
37
+ from ai_agent.memory.context_builder import BuiltMemoryContext, build_memory_context
38
+
39
+ from ai_agent.memory.models import ImportantMemoryEntry, MemoryMessage, MemorySnapshot
40
+
41
+ from ai_agent.memory.snapshot_merge import apply_result, prepare_work
42
+
43
+ from ai_agent.memory.store import MemoryStore
44
+
45
+ from ai_agent.memory.worker import (
46
+
47
+ MemoryTask,
48
+
49
+ MemoryTaskKind,
50
+
51
+ MemoryWorker,
52
+
53
+ expire_old_date_days,
54
+
55
+ )
56
+
57
+
58
+
59
+ MessageRole = Literal["user", "assistant"]
60
+
61
+
62
+
63
+
64
+
65
+ class MemorySystem:
66
+
67
+ """
68
+
69
+ 单会话分层记忆入口:短期、日期、长期与重要记忆。
70
+
71
+
72
+
73
+ Agent 每轮读取已发布视图;压缩在独立线程中执行,完成后发布新视图,不阻塞主推理。
74
+
75
+ """
76
+
77
+
78
+
79
+ def __init__(
80
+
81
+ self,
82
+
83
+ storage_dir: Path | str,
84
+
85
+ *,
86
+
87
+ api_key: str,
88
+
89
+ model: str,
90
+
91
+ base_url: str,
92
+
93
+ config: MemoryConfig | None = None,
94
+
95
+ autostart: bool = True,
96
+
97
+ use_llm_compressor: bool = True,
98
+
99
+ compressor: MemoryCompressor | None = None,
100
+
101
+ ) -> None:
102
+
103
+ if not api_key.strip():
104
+
105
+ raise ValueError("api_key 不能为空")
106
+
107
+ if not model.strip():
108
+
109
+ raise ValueError("model 不能为空")
110
+
111
+ if not base_url.strip():
112
+
113
+ raise ValueError("base_url 不能为空")
114
+
115
+
116
+
117
+ self._storage_dir = Path(storage_dir)
118
+
119
+ self._config = config or MemoryConfig()
120
+
121
+ self._api_key = api_key.strip()
122
+
123
+ self._model = model.strip()
124
+
125
+ self._base_url = base_url.strip().rstrip("/")
126
+
127
+ self._use_llm = use_llm_compressor
128
+
129
+ self._inject_compressor = compressor
130
+
131
+ self._store = MemoryStore(self._storage_dir)
132
+
133
+ self._lock = threading.RLock()
134
+
135
+ self._live = self._store.load()
136
+
137
+ self._agent_view = self._live.model_copy(deep=True)
138
+
139
+ self._worker = MemoryWorker(
140
+
141
+ config=self._config,
142
+
143
+ compressor_factory=self._make_compressor,
144
+
145
+ prepare=self._worker_prepare,
146
+
147
+ commit=self._worker_commit,
148
+
149
+ )
150
+
151
+ if autostart:
152
+
153
+ self._worker.start()
154
+
155
+ self._schedule_expired_dates()
156
+
157
+
158
+
159
+ @property
160
+
161
+ def storage_dir(self) -> Path:
162
+
163
+ """本会话记忆存储目录。"""
164
+
165
+ return self._storage_dir
166
+
167
+
168
+
169
+ @property
170
+
171
+ def config(self) -> MemoryConfig:
172
+
173
+ return self._config
174
+
175
+
176
+
177
+ def append(
178
+
179
+ self,
180
+
181
+ *,
182
+
183
+ speaker: str,
184
+
185
+ role: MessageRole,
186
+
187
+ content: str,
188
+
189
+ at: datetime | None = None,
190
+
191
+ ) -> None:
192
+
193
+ """
194
+
195
+ 追加一条对话到短期记忆,并在超出上限时触发压缩任务。
196
+
197
+
198
+
199
+ Args:
200
+
201
+ speaker: 讲述者显示名(多用户场景区分身份)
202
+
203
+ role: user 或 assistant
204
+
205
+ content: 原文
206
+
207
+ at: 发生时刻,默认 UTC 当前时间
208
+
209
+ """
210
+
211
+ label = speaker.strip()
212
+
213
+ if not label:
214
+
215
+ raise ValueError("speaker 不能为空")
216
+
217
+ text = content.strip()
218
+
219
+ if not text:
220
+
221
+ return
222
+
223
+ stamp = at or datetime.now(timezone.utc)
224
+
225
+ if stamp.tzinfo is None:
226
+
227
+ stamp = stamp.replace(tzinfo=timezone.utc)
228
+
229
+ msg = MemoryMessage(speaker=label, role=role, content=text, at=stamp)
230
+
231
+ with self._lock:
232
+
233
+ self._live.short_term.append(msg)
234
+
235
+ self._sync_agent_short_term()
236
+
237
+ self._store.save_short_term(self._live.short_term)
238
+
239
+ self._maybe_overflow_short_term()
240
+
241
+
242
+
243
+ def remember(self, content: str, *, source: str = "explicit") -> None:
244
+
245
+ """
246
+
247
+ 直接写入重要记忆。
248
+
249
+
250
+
251
+ Args:
252
+
253
+ content: 要记住的文本
254
+
255
+ source: 来源标记
256
+
257
+ """
258
+
259
+ text = content.strip()
260
+
261
+ if not text:
262
+
263
+ return
264
+
265
+ now = datetime.now(timezone.utc)
266
+
267
+ with self._lock:
268
+
269
+ self._live.important.append(
270
+
271
+ ImportantMemoryEntry(at=now, content=text, source=source),
272
+
273
+ )
274
+
275
+ self._sync_agent_important()
276
+
277
+ self._store.save_important(self._live.important)
278
+
279
+ self._maybe_compress_important()
280
+
281
+
282
+
283
+ def build_context(self) -> BuiltMemoryContext:
284
+
285
+ """生成供 Agent.run 使用的系统补充与消息列表(读已发布视图,不等待压缩)。"""
286
+
287
+ view = self._copy_agent_view()
288
+
289
+ return build_memory_context(view)
290
+
291
+
292
+
293
+ def context_for_agent(
294
+
295
+ self,
296
+
297
+ *,
298
+
299
+ system_prompt: str,
300
+
301
+ ) -> tuple[str, list[ChatMessage]]:
302
+
303
+ """
304
+
305
+ 合并系统提示与记忆,返回 (完整 system_prompt, messages)。
306
+
307
+
308
+
309
+ Args:
310
+
311
+ system_prompt: 调用方原始系统提示
312
+
313
+
314
+
315
+ Returns:
316
+
317
+ 拼接记忆后的系统提示与短期消息列表
318
+
319
+ """
320
+
321
+ ctx = self.build_context()
322
+
323
+ if ctx.system_supplement:
324
+
325
+ merged = system_prompt.rstrip() + "\n\n" + ctx.system_supplement
326
+
327
+ else:
328
+
329
+ merged = system_prompt
330
+
331
+ return merged, list(ctx.messages)
332
+
333
+
334
+
335
+ def flush(self, *, timeout: float = 30.0) -> None:
336
+
337
+ """等待后台队列中已有压缩任务完成。"""
338
+
339
+ self._worker.drain(timeout=timeout)
340
+
341
+
342
+
343
+ def shutdown(self, *, join: bool = True, timeout: float = 5.0) -> None:
344
+
345
+ """停止后台线程。"""
346
+
347
+ self._worker.stop(join=join, timeout=timeout)
348
+
349
+
350
+
351
+ def _copy_agent_view(self) -> MemorySnapshot:
352
+
353
+ with self._lock:
354
+
355
+ return self._agent_view.model_copy(deep=True)
356
+
357
+
358
+
359
+ def _publish_agent_view(self) -> None:
360
+
361
+ self._agent_view = self._live.model_copy(deep=True)
362
+
363
+
364
+
365
+ def _sync_agent_short_term(self) -> None:
366
+
367
+ self._agent_view.short_term = [
368
+
369
+ m.model_copy(deep=True) for m in self._live.short_term
370
+
371
+ ]
372
+
373
+
374
+
375
+ def _sync_agent_important(self) -> None:
376
+
377
+ self._agent_view.important = [
378
+
379
+ e.model_copy(deep=True) for e in self._live.important
380
+
381
+ ]
382
+
383
+
384
+
385
+ def _worker_prepare(self, task: MemoryTask) -> CompressionWork | None:
386
+
387
+ with self._lock:
388
+
389
+ return prepare_work(self._live, task, config=self._config)
390
+
391
+
392
+
393
+ def _worker_commit(
394
+
395
+ self,
396
+
397
+ task: MemoryTask,
398
+
399
+ work: CompressionWork,
400
+
401
+ result: CompressionResult,
402
+
403
+ ) -> None:
404
+
405
+ followups: list[MemoryTask] = []
406
+
407
+ with self._lock:
408
+
409
+ followups = apply_result(
410
+
411
+ self._live,
412
+
413
+ task,
414
+
415
+ work,
416
+
417
+ result,
418
+
419
+ config=self._config,
420
+
421
+ )
422
+
423
+ self._publish_agent_view()
424
+
425
+ self._persist_all_layers()
426
+
427
+ if task.kind == MemoryTaskKind.SHORT_TO_DATE:
428
+
429
+ self._maybe_overflow_short_term()
430
+
431
+ if task.kind == MemoryTaskKind.COMPRESS_IMPORTANT:
432
+
433
+ self._maybe_compress_important()
434
+
435
+ if task.kind == MemoryTaskKind.COMPRESS_LONG:
436
+
437
+ if len(self._live.long_term) > self._config.long_term_max_chunks:
438
+
439
+ followups.append(MemoryTask(MemoryTaskKind.COMPRESS_LONG, {}))
440
+
441
+ for follow in followups:
442
+
443
+ self._worker.enqueue(follow)
444
+
445
+
446
+
447
+ def _make_compressor(self) -> MemoryCompressor:
448
+
449
+ if self._inject_compressor is not None:
450
+
451
+ return self._inject_compressor
452
+
453
+ if not self._use_llm:
454
+
455
+ return RuleMemoryCompressor()
456
+
457
+ client = AsyncOpenAI(api_key=self._api_key, base_url=self._base_url)
458
+
459
+ llm = OpenAILLM(client, model=self._model)
460
+
461
+ return LLMMemoryCompressor(llm)
462
+
463
+
464
+
465
+ def _persist_all_layers(self) -> None:
466
+
467
+ self._store.save_short_term(self._live.short_term)
468
+
469
+ active_dates = {day.date for day in self._live.date_days}
470
+
471
+ for day in self._live.date_days:
472
+
473
+ self._store.save_date_day(day)
474
+
475
+ self._store.prune_date_files(active_dates)
476
+
477
+ self._store.save_long_term(self._live.long_term)
478
+
479
+ self._store.save_important(self._live.important)
480
+
481
+
482
+
483
+ def _maybe_overflow_short_term(self) -> None:
484
+
485
+ limit = self._config.short_term_max_messages
486
+
487
+ if len(self._live.short_term) <= limit:
488
+
489
+ return
490
+
491
+ batch = min(
492
+
493
+ self._config.short_term_overflow_batch,
494
+
495
+ len(self._live.short_term),
496
+
497
+ )
498
+
499
+ self._worker.enqueue(
500
+
501
+ MemoryTask(
502
+
503
+ MemoryTaskKind.SHORT_TO_DATE,
504
+
505
+ {"batch_size": batch},
506
+
507
+ ),
508
+
509
+ )
510
+
511
+
512
+
513
+ def _maybe_compress_important(self) -> None:
514
+
515
+ if len(self._live.important) <= self._config.important_max_entries:
516
+
517
+ return
518
+
519
+ self._worker.enqueue(
520
+
521
+ MemoryTask(MemoryTaskKind.COMPRESS_IMPORTANT, {}),
522
+
523
+ )
524
+
525
+
526
+
527
+ def _schedule_expired_dates(self) -> None:
528
+
529
+ with self._lock:
530
+
531
+ expired = expire_old_date_days(
532
+
533
+ self._live,
534
+
535
+ config=self._config,
536
+
537
+ )
538
+
539
+ for day_label in expired:
540
+
541
+ self._worker.enqueue(
542
+
543
+ MemoryTask(
544
+
545
+ MemoryTaskKind.DATE_TO_LONG,
546
+
547
+ {"day": day_label},
548
+
549
+ ),
550
+
551
+ )
552
+
553
+ if len(self._live.long_term) > self._config.long_term_max_chunks:
554
+
555
+ self._worker.enqueue(
556
+
557
+ MemoryTask(MemoryTaskKind.COMPRESS_LONG, {}),
558
+
559
+ )
560
+
561
+
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+
9
+ MessageRole = Literal["user", "assistant"]
10
+
11
+
12
+ class MemoryMessage(BaseModel):
13
+ """短期记忆中的单条对话。"""
14
+
15
+ model_config = ConfigDict(extra="forbid")
16
+
17
+ speaker: str = Field(description="讲述者显示名")
18
+ role: MessageRole = Field(description="OpenAI 角色")
19
+ content: str = Field(description="原文")
20
+ at: datetime = Field(description="发生时刻")
21
+
22
+
23
+ class DateMemoryEntry(BaseModel):
24
+ """日期记忆中的压缩条目。"""
25
+
26
+ model_config = ConfigDict(extra="forbid")
27
+
28
+ at: datetime = Field(description="原话发生时刻")
29
+ speaker: str = Field(description="讲述者显示名")
30
+ summary: str = Field(description="压缩摘要")
31
+
32
+
33
+ class DateMemoryDay(BaseModel):
34
+ """某一天的日期记忆。"""
35
+
36
+ model_config = ConfigDict(extra="forbid")
37
+
38
+ date: str = Field(description="YYYY-MM-DD")
39
+ entries: list[DateMemoryEntry] = Field(default_factory=list)
40
+
41
+
42
+ class LongTermChunk(BaseModel):
43
+ """长期记忆块;越久越模糊。"""
44
+
45
+ model_config = ConfigDict(extra="forbid")
46
+
47
+ created_at: datetime = Field(description="首次写入时刻")
48
+ updated_at: datetime = Field(description="最近更新时刻")
49
+ summary: str = Field(description="模糊化摘要")
50
+ clarity: float = Field(
51
+ default=1.0,
52
+ ge=0.0,
53
+ le=1.0,
54
+ description="清晰程度,1 为最新,趋近 0 表示已融合",
55
+ )
56
+
57
+
58
+ class ImportantMemoryEntry(BaseModel):
59
+ """需长期保留的事实或偏好。"""
60
+
61
+ model_config = ConfigDict(extra="forbid")
62
+
63
+ at: datetime = Field(description="记录时刻")
64
+ content: str = Field(description="要记住的内容")
65
+ source: str = Field(default="", description="来源说明,如讲述者或压缩任务")
66
+
67
+
68
+ class MemorySnapshot(BaseModel):
69
+ """单会话记忆快照,对应存储目录根。"""
70
+
71
+ model_config = ConfigDict(extra="forbid")
72
+
73
+ short_term: list[MemoryMessage] = Field(default_factory=list)
74
+ date_days: list[DateMemoryDay] = Field(default_factory=list)
75
+ long_term: list[LongTermChunk] = Field(default_factory=list)
76
+ important: list[ImportantMemoryEntry] = Field(default_factory=list)