spl-agent-engine 0.2.0__tar.gz → 0.3.0__tar.gz

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 (20) hide show
  1. {spl_agent_engine-0.2.0/spl_agent_engine.egg-info → spl_agent_engine-0.3.0}/PKG-INFO +1 -1
  2. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/pyproject.toml +1 -1
  3. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/__init__.py +18 -3
  4. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/core.py +135 -11
  5. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0/spl_agent_engine.egg-info}/PKG-INFO +1 -1
  6. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/LICENSE +0 -0
  7. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/README.md +0 -0
  8. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/setup.cfg +0 -0
  9. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/feature/__init__.py +0 -0
  10. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/feature/bias.py +0 -0
  11. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/feature/goal.py +0 -0
  12. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/feature/identity.py +0 -0
  13. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/feature/language_style.py +0 -0
  14. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/feature/value.py +0 -0
  15. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/feature/world.py +0 -0
  16. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine/py.typed +0 -0
  17. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine.egg-info/SOURCES.txt +0 -0
  18. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine.egg-info/dependency_links.txt +0 -0
  19. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine.egg-info/requires.txt +0 -0
  20. {spl_agent_engine-0.2.0 → spl_agent_engine-0.3.0}/spl_agent_engine.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spl-agent-engine
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Anthropomorphic psychology engine built on SPL Pure Core V8.0 — deterministic emotion, trauma, memory, trust, and defense mechanisms.
5
5
  Author-email: NOHN-AI <ai@nohnlins.com>
6
6
  Maintainer-email: NOHN-AI <ai@nohnlins.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spl-agent-engine"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Anthropomorphic psychology engine built on SPL Pure Core V8.0 — deterministic emotion, trauma, memory, trust, and defense mechanisms."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.8"
@@ -19,10 +19,25 @@ Usage:
19
19
  print(core.fluid)
20
20
  """
21
21
 
22
- from .core import AuditLogger, SPLPureCoreV7_3, NarrativeMapper
22
+ from .core import (
23
+ AuditLogger,
24
+ SPLPureCoreV7_3,
25
+ NarrativeMapper,
26
+ TokenUsage,
27
+ LLMAdapter,
28
+ OpenAIAdapter,
29
+ ClaudeAdapter,
30
+ ChainAdapter,
31
+ generate_line_with_llm,
32
+ )
23
33
 
24
34
  # Canonical name for V8.0 (class name kept for backward compatibility)
25
35
  SPLPureCore = SPLPureCoreV7_3
26
36
 
27
- __version__ = "0.2.0"
28
- __all__ = ["SPLPureCore", "SPLPureCoreV7_3", "NarrativeMapper", "AuditLogger"]
37
+ __version__ = "0.3.0"
38
+ __all__ = [
39
+ "SPLPureCore", "SPLPureCoreV7_3", "NarrativeMapper", "AuditLogger",
40
+ "TokenUsage",
41
+ "LLMAdapter", "OpenAIAdapter", "ClaudeAdapter", "ChainAdapter",
42
+ "generate_line_with_llm",
43
+ ]
@@ -4,7 +4,7 @@ import json
4
4
  import os
5
5
  import datetime
6
6
  from dataclasses import dataclass, field
7
- from typing import Dict, Any, List, Optional
7
+ from typing import Dict, Any, List, Optional, Tuple, Union
8
8
 
9
9
 
10
10
  # ================================================================
@@ -75,10 +75,54 @@ class AuditLogger:
75
75
  "memory_count", "expected_count")
76
76
  return {k: snap.get(k) for k in keys if k in snap}
77
77
 
78
+ def log_llm_call(self, model: str, prompt_preview: str,
79
+ usage: Optional["TokenUsage"] = None,
80
+ duration_ms: float = 0.0,
81
+ success: bool = True,
82
+ error: str = "") -> None:
83
+ """记录一次 LLM 调用(token 用量、耗时、成功/失败)。
84
+
85
+ 写入失败静默跳过,绝不阻断引擎核心。
86
+ """
87
+ if not self.enabled:
88
+ return
89
+ try:
90
+ entry = {
91
+ "seq": self._entry_count,
92
+ "ts": datetime.datetime.now().isoformat(timespec="milliseconds"),
93
+ "engine": self.ENGINE_VERSION,
94
+ "session": self.session_id,
95
+ "event": "llm_call",
96
+ "model": model,
97
+ "prompt_preview": prompt_preview[:200],
98
+ "success": success,
99
+ "duration_ms": round(duration_ms, 2),
100
+ "usage": {
101
+ "prompt_tokens": usage.prompt_tokens if usage else 0,
102
+ "completion_tokens": usage.completion_tokens if usage else 0,
103
+ "total_tokens": usage.total_tokens if usage else 0,
104
+ } if usage else None,
105
+ "error": error or None,
106
+ }
107
+ with open(self.log_file, "a", encoding="utf-8") as f:
108
+ f.write(json.dumps(entry, ensure_ascii=False, default=str) + "\n")
109
+ self._entry_count += 1
110
+ except Exception:
111
+ pass
112
+
78
113
  def export_path(self) -> Optional[str]:
79
114
  return self.log_file if os.path.exists(self.log_file) else None
80
115
 
81
116
 
117
+ @dataclass
118
+ class TokenUsage:
119
+ """LLM 调用 token 用量统计。"""
120
+ prompt_tokens: int = 0
121
+ completion_tokens: int = 0
122
+ total_tokens: int = 0
123
+ model: str = ""
124
+
125
+
82
126
  # ================================================================
83
127
  # 1. 外部叙事映射器("自设:偏见/世界观/价值观"——纯外部策略)
84
128
  # 用户可随意替换,无需修改核心代码。
@@ -1309,7 +1353,9 @@ class LLMAdapter(ABC):
1309
1353
  style_example: str = "",
1310
1354
  system_prompt: str = "",
1311
1355
  temperature: float = 0.7,
1312
- max_tokens: int = 500) -> Optional[str]:
1356
+ max_tokens: int = 500,
1357
+ return_usage: bool = False
1358
+ ) -> Union[Optional[str], Tuple[Optional[str], Optional["TokenUsage"]]]:
1313
1359
  """
1314
1360
  根据 prompt_injection 生成台词。
1315
1361
 
@@ -1319,9 +1365,11 @@ class LLMAdapter(ABC):
1319
1365
  system_prompt: 可选系统提示词,覆盖默认
1320
1366
  temperature: 采样温度
1321
1367
  max_tokens: 最大生成 token 数
1368
+ return_usage: True 时返回 (台词, TokenUsage) 元组
1322
1369
 
1323
1370
  Returns:
1324
- 生成的台词字符串,失败返回 None
1371
+ return_usage=False: 生成的台词字符串,失败返回 None
1372
+ return_usage=True: (台词, TokenUsage) 元组
1325
1373
  """
1326
1374
  ...
1327
1375
 
@@ -1350,7 +1398,9 @@ class OpenAIAdapter(LLMAdapter):
1350
1398
  style_example: str = "",
1351
1399
  system_prompt: str = "",
1352
1400
  temperature: float = 0.7,
1353
- max_tokens: int = 500) -> Optional[str]:
1401
+ max_tokens: int = 500,
1402
+ return_usage: bool = False
1403
+ ) -> Union[Optional[str], Tuple[Optional[str], Optional[TokenUsage]]]:
1354
1404
  import urllib.request
1355
1405
  import urllib.error
1356
1406
 
@@ -1383,8 +1433,22 @@ class OpenAIAdapter(LLMAdapter):
1383
1433
  body = json.loads(resp.read().decode("utf-8"))
1384
1434
  choice = body.get("choices", [{}])[0]
1385
1435
  content = choice.get("message", {}).get("content", "")
1386
- return content.strip() if content else None
1436
+ result = content.strip() if content else None
1437
+
1438
+ usage_data = body.get("usage", {}) or {}
1439
+ usage = TokenUsage(
1440
+ prompt_tokens=usage_data.get("prompt_tokens", 0),
1441
+ completion_tokens=usage_data.get("completion_tokens", 0),
1442
+ total_tokens=usage_data.get("total_tokens", 0),
1443
+ model=body.get("model", self.model),
1444
+ ) if usage_data else None
1445
+
1446
+ if return_usage:
1447
+ return result, usage
1448
+ return result
1387
1449
  except (urllib.error.URLError, json.JSONDecodeError, KeyError) as e:
1450
+ if return_usage:
1451
+ return None, None
1388
1452
  return None
1389
1453
 
1390
1454
 
@@ -1412,7 +1476,9 @@ class ClaudeAdapter(LLMAdapter):
1412
1476
  style_example: str = "",
1413
1477
  system_prompt: str = "",
1414
1478
  temperature: float = 0.7,
1415
- max_tokens: int = 500) -> Optional[str]:
1479
+ max_tokens: int = 500,
1480
+ return_usage: bool = False
1481
+ ) -> Union[Optional[str], Tuple[Optional[str], Optional[TokenUsage]]]:
1416
1482
  import urllib.request
1417
1483
  import urllib.error
1418
1484
 
@@ -1445,8 +1511,24 @@ class ClaudeAdapter(LLMAdapter):
1445
1511
  with urllib.request.urlopen(req, timeout=self.timeout) as resp:
1446
1512
  body = json.loads(resp.read().decode("utf-8"))
1447
1513
  content = body.get("content", [{}])[0].get("text", "")
1448
- return content.strip() if content else None
1514
+ result = content.strip() if content else None
1515
+
1516
+ usage_data = body.get("usage", {}) or {}
1517
+ prompt_tok = usage_data.get("input_tokens", 0)
1518
+ completion_tok = usage_data.get("output_tokens", 0)
1519
+ usage = TokenUsage(
1520
+ prompt_tokens=prompt_tok,
1521
+ completion_tokens=completion_tok,
1522
+ total_tokens=prompt_tok + completion_tok,
1523
+ model=body.get("model", self.model),
1524
+ ) if usage_data else None
1525
+
1526
+ if return_usage:
1527
+ return result, usage
1528
+ return result
1449
1529
  except (urllib.error.URLError, json.JSONDecodeError, KeyError) as e:
1530
+ if return_usage:
1531
+ return None, None
1450
1532
  return None
1451
1533
 
1452
1534
 
@@ -1472,24 +1554,45 @@ class ChainAdapter(LLMAdapter):
1472
1554
  style_example: str = "",
1473
1555
  system_prompt: str = "",
1474
1556
  temperature: float = 0.7,
1475
- max_tokens: int = 500) -> Optional[str]:
1476
- result = self.primary.generate(
1557
+ max_tokens: int = 500,
1558
+ return_usage: bool = False
1559
+ ) -> Union[Optional[str], Tuple[Optional[str], Optional[TokenUsage]]]:
1560
+ result, usage = self.primary.generate(
1477
1561
  prompt_injection=prompt_injection,
1478
1562
  style_example=style_example,
1479
1563
  system_prompt=system_prompt,
1480
1564
  temperature=temperature,
1481
1565
  max_tokens=max_tokens,
1566
+ return_usage=True,
1482
1567
  )
1483
1568
  if result is not None:
1569
+ if return_usage:
1570
+ return result, usage
1484
1571
  return result
1485
1572
  if self.fallback is not None:
1486
- return self.fallback.generate(
1573
+ result2, usage2 = self.fallback.generate(
1487
1574
  prompt_injection=prompt_injection,
1488
1575
  style_example=style_example,
1489
1576
  system_prompt=system_prompt,
1490
1577
  temperature=temperature,
1491
1578
  max_tokens=max_tokens,
1579
+ return_usage=True,
1492
1580
  )
1581
+ if return_usage:
1582
+ # 合并两次调用的 token 用量
1583
+ if usage and usage2:
1584
+ merged = TokenUsage(
1585
+ prompt_tokens=usage.prompt_tokens + usage2.prompt_tokens,
1586
+ completion_tokens=usage.completion_tokens + usage2.completion_tokens,
1587
+ total_tokens=usage.total_tokens + usage2.total_tokens,
1588
+ model=f"{usage.model}->{usage2.model}",
1589
+ )
1590
+ else:
1591
+ merged = usage2 or usage
1592
+ return result2, merged
1593
+ return result2
1594
+ if return_usage:
1595
+ return None, usage
1493
1596
  return None
1494
1597
 
1495
1598
 
@@ -1501,6 +1604,7 @@ def generate_line_with_llm(
1501
1604
  core_snapshot: dict,
1502
1605
  expression,
1503
1606
  adapter: LLMAdapter,
1607
+ audit_logger: Optional["AuditLogger"] = None,
1504
1608
  ) -> str:
1505
1609
  """
1506
1610
  便利函数:引擎渲染风格 → LLM 生成台词 → 失败回退到确定性生成。
@@ -1510,18 +1614,38 @@ def generate_line_with_llm(
1510
1614
  core_snapshot: SPL 核心快照(含 fluid/mood/self_esteem 等)
1511
1615
  expression: 表达意图对象(含 expression_mode / emotion_hidden / silence 等)
1512
1616
  adapter: LLMAdapter 实例
1617
+ audit_logger: 可选 AuditLogger,传入则记录 LLM 调用与 token 用量
1513
1618
 
1514
1619
  Returns:
1515
1620
  台词字符串(LLM 成功=LLM 产出,失败=引擎确定性产出)
1516
1621
  """
1622
+ import time as _time
1623
+
1517
1624
  rendered = engine.render_style(core_snapshot, expression)
1518
1625
  if rendered.silence_hint:
1519
1626
  return rendered.silence_hint
1520
1627
 
1521
- line = adapter.generate(
1628
+ t0 = _time.perf_counter()
1629
+ line, usage = adapter.generate(
1522
1630
  prompt_injection=rendered.prompt_injection,
1523
1631
  style_example=rendered.style_example,
1632
+ return_usage=True,
1524
1633
  )
1634
+ duration_ms = (_time.perf_counter() - t0) * 1000
1635
+
1636
+ if audit_logger is not None:
1637
+ model = getattr(adapter, "model", "") or (
1638
+ usage.model if usage else ""
1639
+ )
1640
+ audit_logger.log_llm_call(
1641
+ model=model,
1642
+ prompt_preview=rendered.prompt_injection,
1643
+ usage=usage,
1644
+ duration_ms=duration_ms,
1645
+ success=line is not None,
1646
+ error="" if line is not None else "llm_failed",
1647
+ )
1648
+
1525
1649
  if line is not None:
1526
1650
  return line
1527
1651
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spl-agent-engine
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Anthropomorphic psychology engine built on SPL Pure Core V8.0 — deterministic emotion, trauma, memory, trust, and defense mechanisms.
5
5
  Author-email: NOHN-AI <ai@nohnlins.com>
6
6
  Maintainer-email: NOHN-AI <ai@nohnlins.com>