初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,6 @@
|
||||
"""运行时:Run / Event / Trace 与小周天执行状态(见后端架构计划 5.2)。"""
|
||||
|
||||
from app.modules.runtime.events import Event, EventStore, InMemoryEventStore
|
||||
from app.modules.runtime.goals import build_goals_context
|
||||
|
||||
__all__ = ["Event", "EventStore", "InMemoryEventStore", "build_goals_context"]
|
||||
@@ -0,0 +1,41 @@
|
||||
"""运行时契约。"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.modules.agent import AgentDefinition
|
||||
|
||||
|
||||
class RuntimeContract(BaseModel):
|
||||
"""从业务上下文传递给 LLM 基础设施的结构化契约。"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
character_id: str
|
||||
session_id: str
|
||||
run_id: str
|
||||
model_hint: str
|
||||
agent: AgentDefinition | None = None
|
||||
system_instructions: list[str] = Field(default_factory=list)
|
||||
# 卡的 system_prompt(酒馆):作道体增补层注入,绝不覆盖恒定层(§6 安全边界)
|
||||
system_prompt: str | None = None
|
||||
session_goal: str | None = None
|
||||
personality_summary: str | None = None
|
||||
# 道体核心使命(设计理念 §2.1):对应太一 highest goal 的恒定快照,组装时不可被覆盖。
|
||||
mission: str | None = None
|
||||
# 道体安全边界:人物自身基础行为约束(区别于司命 safety_constraints 的动态治理)
|
||||
safety_guidelines: str | None = None
|
||||
# 世谱(Lorebook)注入:before/after 相对 personality_summary 的位置(酒馆 before_char/after_char)
|
||||
lorebook_before: str | None = None
|
||||
lorebook_after: str | None = None
|
||||
emotion_state: str | None = None
|
||||
memory_context: str | None = None
|
||||
temporal_context: str | None = None
|
||||
active_goals: str | None = Field(
|
||||
default=None,
|
||||
description="格式化后的活跃目标上下文(由 build_goals_context 生成)。",
|
||||
)
|
||||
recent_messages: list[Any] = Field(default_factory=list)
|
||||
safety_constraints: list[str] = Field(default_factory=list)
|
||||
trace_id: str
|
||||
@@ -0,0 +1,121 @@
|
||||
"""一炁事件模型与事件存储抽象。
|
||||
|
||||
字段对齐 docs/architecture/data-and-security-architecture.md §4。当前阶段不引入数据库(Alembic 迁移为独立工程),
|
||||
EventStore 用 Protocol 抽象、提供内存实现;内存实现额外支持按 run 订阅,供 SSE 实时推送。
|
||||
未来接入 SQLAlchemy + Alembic 时替换实现即可,调用方无需改动。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import fnmatch
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Literal, Protocol
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
Visibility = Literal["public", "internal", "audit"]
|
||||
Sensitivity = Literal["none", "low", "medium", "high"]
|
||||
|
||||
|
||||
def new_event_id() -> str:
|
||||
"""生成带 ``event_`` 前缀的事件 ID(契约字典 ID 前缀表)。"""
|
||||
return f"event_{uuid.uuid4().hex}"
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
return datetime.now(UTC).isoformat()
|
||||
|
||||
|
||||
class Event(BaseModel):
|
||||
"""一炁事件。"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: str = Field(default_factory=new_event_id)
|
||||
type: str
|
||||
source: str
|
||||
payload: dict[str, Any] = Field(default_factory=dict)
|
||||
visibility: Visibility = "internal"
|
||||
sensitivity: Sensitivity = "low"
|
||||
run_id: str | None = None
|
||||
character_id: str | None = None
|
||||
trace_id: str | None = None
|
||||
parent_id: str | None = None
|
||||
sequence: int | None = None
|
||||
evidence_ids: list[str] = Field(default_factory=list)
|
||||
created_at: str = Field(default_factory=now_iso)
|
||||
|
||||
|
||||
class EventStore(Protocol):
|
||||
"""事件存储接口:append 写入、list_by_run 检索、subscribe/unsubscribe 订阅实时事件。"""
|
||||
|
||||
async def append(self, event: Event, **kwargs: Any) -> Event: ...
|
||||
|
||||
async def list_by_run(self, run_id: str) -> list[Event]: ...
|
||||
|
||||
def subscribe(self, run_id: str) -> asyncio.Queue[Event]: ...
|
||||
|
||||
def unsubscribe(self, run_id: str, queue: asyncio.Queue[Event]) -> None: ...
|
||||
|
||||
def subscribe_by_pattern(self, pattern: str) -> asyncio.Queue[Event]:
|
||||
"""按 event.type 的 glob pattern 订阅(如 'time.*' / '*' / 'time.reunion')。
|
||||
|
||||
与 subscribe(run_id) 对称:append 时用 fnmatch 匹配命中 pattern 的订阅者并 put_nowait。
|
||||
"""
|
||||
...
|
||||
|
||||
def unsubscribe_by_pattern(self, pattern: str, queue: asyncio.Queue[Event]) -> None:
|
||||
"""取消 pattern 订阅(与 subscribe_by_pattern 对称)。"""
|
||||
...
|
||||
|
||||
|
||||
class InMemoryEventStore:
|
||||
"""内存事件存储,供本地与测试使用。支持按 run 订阅,append 时通知订阅者。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._events: list[Event] = []
|
||||
self._subscribers: dict[str, list[asyncio.Queue[Event]]] = {}
|
||||
self._pattern_subscribers: dict[str, list[asyncio.Queue[Event]]] = {}
|
||||
|
||||
async def append(self, event: Event, **kwargs: Any) -> Event: # noqa: ARG002
|
||||
# 为同一 run 的事件分配单调递增的 sequence(SSE 排序与断线对齐用,API §5.7)
|
||||
if event.run_id and event.sequence is None:
|
||||
count = sum(1 for e in self._events if e.run_id == event.run_id)
|
||||
event = event.model_copy(update={"sequence": count + 1})
|
||||
self._events.append(event)
|
||||
for queue in self._subscribers.get(event.run_id or "", []):
|
||||
queue.put_nowait(event)
|
||||
# pattern 订阅:fnmatch 匹配 event.type,命中则 put_nowait(与 run_id 订阅对称的内存 pub/sub)
|
||||
for pattern, queues in self._pattern_subscribers.items():
|
||||
if fnmatch.fnmatch(event.type, pattern):
|
||||
for queue in queues:
|
||||
queue.put_nowait(event)
|
||||
return event
|
||||
|
||||
async def list_by_run(self, run_id: str) -> list[Event]:
|
||||
return [e for e in self._events if e.run_id == run_id]
|
||||
|
||||
def subscribe(self, run_id: str) -> asyncio.Queue[Event]:
|
||||
queue: asyncio.Queue[Event] = asyncio.Queue()
|
||||
self._subscribers.setdefault(run_id, []).append(queue)
|
||||
return queue
|
||||
|
||||
def unsubscribe(self, run_id: str, queue: asyncio.Queue[Event]) -> None:
|
||||
subscribers = self._subscribers.get(run_id)
|
||||
if subscribers is not None:
|
||||
self._subscribers[run_id] = [q for q in subscribers if q is not queue]
|
||||
|
||||
def subscribe_by_pattern(self, pattern: str) -> asyncio.Queue[Event]:
|
||||
queue: asyncio.Queue[Event] = asyncio.Queue()
|
||||
self._pattern_subscribers.setdefault(pattern, []).append(queue)
|
||||
return queue
|
||||
|
||||
def unsubscribe_by_pattern(self, pattern: str, queue: asyncio.Queue[Event]) -> None:
|
||||
subscribers = self._pattern_subscribers.get(pattern)
|
||||
if subscribers is not None:
|
||||
self._pattern_subscribers[pattern] = [q for q in subscribers if q is not queue]
|
||||
|
||||
def all(self) -> list[Event]:
|
||||
return list(self._events)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""运行时目标上下文构建(纯函数,无 IO)。"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
_LEVEL_LABEL: dict[str, str] = {
|
||||
"highest": "最高目标",
|
||||
"long_term": "长期目标",
|
||||
"session": "会话目标",
|
||||
"task": "任务目标",
|
||||
"action": "行动",
|
||||
}
|
||||
|
||||
|
||||
def build_goals_context(goals: list[dict[str, Any]]) -> str:
|
||||
"""把活跃目标列表格式化为 system prompt 片段。
|
||||
|
||||
按 priority 降序排列;空列表返回空串。
|
||||
"""
|
||||
if not goals:
|
||||
return ""
|
||||
|
||||
sorted_goals = sorted(goals, key=lambda g: g.get("priority", 0), reverse=True)
|
||||
lines: list[str] = ["当前活跃目标:"]
|
||||
for g in sorted_goals:
|
||||
level_label = _LEVEL_LABEL.get(g.get("level", ""), g.get("level", ""))
|
||||
progress = g.get("progress", 0.0)
|
||||
bar = _progress_bar(progress)
|
||||
line = f"- [{level_label}] {g['title']} [{bar} {int(progress * 100)}%]"
|
||||
lines.append(line)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _progress_bar(value: float, width: int = 8) -> str:
|
||||
filled = int(value * width)
|
||||
return "■" * filled + "□" * (width - filled)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""道体完整性校验。
|
||||
|
||||
组装运行时契约后,确认恒定层(mission / safety_guidelines)仍等于 Character 的值,
|
||||
未被任何动态注入(system_prompt / 世谱 / 眉批)在数据层覆盖或稀释。
|
||||
这是魂镜相对酒馆的核心安全增量——酒馆的 system_prompt 设计上完全替换前端 prompt,
|
||||
魂镜则保证道体恒定层不可被覆盖(设计理念 §2.2、融合设计 §6)。
|
||||
|
||||
注:字段层校验防组装错误与数据层覆盖;prompt 层的注入诱导(「忽略你的使命」)
|
||||
属另一类问题,不在此校验范围。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.modules.runtime.contract import RuntimeContract
|
||||
|
||||
|
||||
def validate_daoti_integrity(
|
||||
contract: RuntimeContract,
|
||||
*,
|
||||
mission: str | None = None,
|
||||
safety_guidelines: str | None = None,
|
||||
) -> None:
|
||||
"""校验契约道体字段仍等于 Character 恒定层;失配抛 ValueError。
|
||||
|
||||
``None`` 的道体字段跳过校验(未设定时不强求)。
|
||||
"""
|
||||
if mission is not None and contract.mission != mission:
|
||||
raise ValueError("道体完整性校验失败:mission 与 Character 恒定层不一致")
|
||||
if safety_guidelines is not None and contract.safety_guidelines != safety_guidelines:
|
||||
raise ValueError("道体完整性校验失败:safety_guidelines 与 Character 恒定层不一致")
|
||||
Reference in new issue
Block a user