feat: 角色定义可插拔(RoleSpec+辅助函数)+记忆分域(scope:global/pipeline/project/user)

This commit is contained in:
yumoqing 2026-08-15 09:46:42 +08:00
parent f8036d5300
commit b9b6403de3
3 changed files with 109 additions and 12 deletions

View File

@ -30,11 +30,16 @@ from .agent_config import (
) )
from .ability import ( from .ability import (
PipelineAbility, PipelineAbility,
RoleSpec,
register_ability, register_ability,
get_ability, get_ability,
list_abilities, list_abilities,
get_ability_tools, get_ability_tools,
get_ability_handler, get_ability_handler,
get_role_spec,
normalize_role,
get_next_role,
list_roles,
) )
from .slash import ( from .slash import (
SlashCommand, SlashCommand,

View File

@ -19,9 +19,25 @@ from typing import Callable, Dict, List, Optional
logger = logging.getLogger("pipeline.ability") logger = logging.getLogger("pipeline.ability")
@dataclass
class RoleSpec:
"""产线内角色定义(可插拔)。
一个角色 = 工具白名单 + 专属 prompt + 专属模型 + 任务链位置 + 别名。
产线能力包通过 roles 字段定义自己的角色集(如 SDLC 的 requirement/design/develop/test/deploy)。
"""
name: str # "develop"
description: str = "" # 角色职责描述
aliases: List[str] = field(default_factory=list) # ["dev", "developer"]
system_prompt: str = "" # 角色专属 prompt(职责 + 产出要求)
next_role: str = "" # 任务链下一角色(空 = 终结)
model_name: str = "" # 角色专属模型(空 = 继承产线)
tools: List[str] = field(default_factory=list) # 工具白名单(空 = 产线全部工具)
@dataclass @dataclass
class PipelineAbility: class PipelineAbility:
"""产线能力包:一个产线 = 一组工具 + 一段 prompt + 一组 handler。 """产线能力包:一个产线 = 一组工具 + 一段 prompt + 一组 handler + 一组角色。
handlers 签名统一为:async def handler(sor, params, ctx) -> str handlers 签名统一为:async def handler(sor, params, ctx) -> str
- sor: 已打开的 pipeline DB context(由 AgentExecutor 统一管理) - sor: 已打开的 pipeline DB context(由 AgentExecutor 统一管理)
@ -33,6 +49,7 @@ class PipelineAbility:
tools: List = field(default_factory=list) # [ToolDefinition] tools: List = field(default_factory=list) # [ToolDefinition]
system_prompt: str = "" # 产线专属 prompt 片段(追加到通用心智后) system_prompt: str = "" # 产线专属 prompt 片段(追加到通用心智后)
handlers: Dict[str, Callable] = field(default_factory=dict) # {tool_name: handler} handlers: Dict[str, Callable] = field(default_factory=dict) # {tool_name: handler}
roles: List[RoleSpec] = field(default_factory=list) # 产线角色集
# ── 全局注册表 ── # ── 全局注册表 ──
@ -71,3 +88,37 @@ def get_ability_handler(pipeline_id: str, tool_name: str) -> Optional[Callable]:
if not a: if not a:
return None return None
return a.handlers.get(tool_name) return a.handlers.get(tool_name)
# ── 角色查询 ──
def get_role_spec(pipeline_id: str, role: str) -> Optional[RoleSpec]:
"""取某产线某角色的定义(按 name 或 alias 匹配)。"""
a = get_ability(pipeline_id)
if not a or not role:
return None
r = (role or "").strip().lower()
for spec in a.roles:
if spec.name.lower() == r or r in [x.lower() for x in spec.aliases]:
return spec
return None
def normalize_role(pipeline_id: str, role: str) -> str:
"""角色标准化:别名 → 规范名。能力包未定义时原样返回(lower)。"""
spec = get_role_spec(pipeline_id, role)
if spec:
return spec.name
return (role or "").strip().lower()
def get_next_role(pipeline_id: str, role: str) -> str:
"""任务链下一角色(空 = 终结)。"""
spec = get_role_spec(pipeline_id, role)
return spec.next_role if spec else ""
def list_roles(pipeline_id: str) -> List[RoleSpec]:
"""列出某产线的全部角色。"""
a = get_ability(pipeline_id)
return a.roles if a else []

View File

@ -25,6 +25,8 @@ class MemoryEntry:
key: str # 唯一标识 key: str # 唯一标识
content: str # 记忆内容 content: str # 记忆内容
category: str = "memory" # "user" | "memory" category: str = "memory" # "user" | "memory"
scope: str = "global" # "global" | "pipeline" | "project" | "user"(归属范围)
scope_id: str = "" # pipeline_id / project_id / user_id
priority: int = 0 # 优先级(越高越重要) priority: int = 0 # 优先级(越高越重要)
created_at: float = 0.0 created_at: float = 0.0
updated_at: float = 0.0 updated_at: float = 0.0
@ -35,6 +37,8 @@ class MemoryEntry:
"key": self.key, "key": self.key,
"content": self.content, "content": self.content,
"category": self.category, "category": self.category,
"scope": self.scope,
"scope_id": self.scope_id,
"priority": self.priority, "priority": self.priority,
"created_at": self.created_at, "created_at": self.created_at,
"updated_at": self.updated_at, "updated_at": self.updated_at,
@ -47,6 +51,8 @@ class MemoryEntry:
key=data.get("key", ""), key=data.get("key", ""),
content=data.get("content", ""), content=data.get("content", ""),
category=data.get("category", "memory"), category=data.get("category", "memory"),
scope=data.get("scope", "global"),
scope_id=data.get("scope_id", ""),
priority=data.get("priority", 0), priority=data.get("priority", 0),
created_at=data.get("created_at", time.time()), created_at=data.get("created_at", time.time()),
updated_at=data.get("updated_at", time.time()), updated_at=data.get("updated_at", time.time()),
@ -82,8 +88,8 @@ class MemoryStore:
db = DBPools() db = DBPools()
async with db.sqlorContext("pipeline") as sor: async with db.sqlorContext("pipeline") as sor:
recs = await sor.sqlExe( recs = await sor.sqlExe(
"SELECT memory_key, content, category, priority, created_at, updated_at, access_count " "SELECT memory_key, content, category, scope, scope_id, priority, created_at, updated_at, access_count "
"FROM pipeline_user_memory ORDER BY priority DESC, updated_at DESC LIMIT 200", "FROM pipeline_user_memory ORDER BY priority DESC, updated_at DESC LIMIT 500",
{}, {},
) )
for r in (recs or []): for r in (recs or []):
@ -91,6 +97,8 @@ class MemoryStore:
key=getattr(r, "memory_key", ""), key=getattr(r, "memory_key", ""),
content=getattr(r, "content", ""), content=getattr(r, "content", ""),
category=getattr(r, "category", "memory"), category=getattr(r, "category", "memory"),
scope=getattr(r, "scope", "global"),
scope_id=getattr(r, "scope_id", ""),
priority=getattr(r, "priority", 0), priority=getattr(r, "priority", 0),
created_at=_ts(getattr(r, "created_at", None)), created_at=_ts(getattr(r, "created_at", None)),
updated_at=_ts(getattr(r, "updated_at", None)), updated_at=_ts(getattr(r, "updated_at", None)),
@ -105,8 +113,12 @@ class MemoryStore:
self._cache_loaded = True self._cache_loaded = True
async def add(self, content: str, category: str = "memory", async def add(self, content: str, category: str = "memory",
priority: int = PRIORITY_MEDIUM, key: str = None): priority: int = PRIORITY_MEDIUM, key: str = None,
"""添加一条记忆。自动生成 key(取内容前80字符的哈希)。""" scope: str = "global", scope_id: str = ""):
"""添加一条记忆。自动生成 key(取内容前80字符的哈希)。
scope 分域:global(通用)/ pipeline(产线)/ project(项目)/ user(用户个人)。
"""
await self._ensure_cache() await self._ensure_cache()
if not key: if not key:
@ -117,6 +129,8 @@ class MemoryStore:
key=key, key=key,
content=content, content=content,
category=category, category=category,
scope=scope,
scope_id=scope_id,
priority=priority, priority=priority,
created_at=now, created_at=now,
updated_at=now, updated_at=now,
@ -136,8 +150,12 @@ class MemoryStore:
# 淘汰低优先级旧条目 # 淘汰低优先级旧条目
await self._evict(max_entries=100) await self._evict(max_entries=100)
async def get(self, category: str = None, key: str = None) -> List[MemoryEntry]: async def get(self, category: str = None, key: str = None,
"""获取记忆。可按分类和 key 筛选。""" scope: str = None, scope_id: str = None) -> List[MemoryEntry]:
"""获取记忆。可按分类/key/scope 筛选。
scope 过滤:scope=None 返回全部;scope 指定时返回该 scope 且 scope_id 匹配(或 scope_id=None 时该 scope 全部)。
"""
await self._ensure_cache() await self._ensure_cache()
results = [] results = []
@ -147,6 +165,11 @@ class MemoryStore:
for k, entry in self._cache[cat].items(): for k, entry in self._cache[cat].items():
if key and k != key: if key and k != key:
continue continue
if scope is not None:
if entry.scope != scope:
continue
if scope_id is not None and entry.scope_id != scope_id:
continue
results.append(entry) results.append(entry)
entry.access_count += 1 entry.access_count += 1
@ -171,15 +194,30 @@ class MemoryStore:
except Exception as e: except Exception as e:
logger.error(f"MemoryStore remove failed: {e}") logger.error(f"MemoryStore remove failed: {e}")
async def build_prompt_block(self, category: str = None, max_entries: int = 20) -> str: async def build_prompt_block(self, category: str = None, max_entries: int = 20,
scope: str = None, scope_id: str = None) -> str:
"""构建注入 system prompt 的记忆段落。 """构建注入 system prompt 的记忆段落。
指定 scope 时加载「global(通用)+ 指定 scope(专属)」的记忆,
实现通用记忆 + 产线/项目专属记忆的叠加注入。
高优先级记忆注入完整内容,低优先级只注入摘要。 高优先级记忆注入完整内容,低优先级只注入摘要。
""" """
entries = await self.get(category) if scope:
# 通用 + 专属叠加
global_entries = await self.get(category, scope="global")
scoped_entries = await self.get(category, scope=scope, scope_id=scope_id)
# 去重(同 key 专属覆盖通用)
merged = {e.key: e for e in global_entries}
merged.update({e.key: e for e in scoped_entries})
entries = list(merged.values())
else:
entries = await self.get(category)
if not entries: if not entries:
return "" return ""
entries.sort(key=lambda e: e.priority, reverse=True)
blocks = [] blocks = []
high_priority = [e for e in entries if e.priority >= self.PRIORITY_MEDIUM] high_priority = [e for e in entries if e.priority >= self.PRIORITY_MEDIUM]
low_priority = [e for e in entries if e.priority < self.PRIORITY_MEDIUM] low_priority = [e for e in entries if e.priority < self.PRIORITY_MEDIUM]
@ -194,14 +232,15 @@ class MemoryStore:
return "\n".join(blocks) if blocks else "" return "\n".join(blocks) if blocks else ""
async def _db_upsert(self, entry: MemoryEntry): async def _db_upsert(self, entry: MemoryEntry):
"""写入/更新 DB""" """写入/更新 DB(同 key + category + scope + scope_id 才 upsert)。"""
from sqlor.dbpools import DBPools from sqlor.dbpools import DBPools
db = DBPools() db = DBPools()
async with db.sqlorContext("pipeline") as sor: async with db.sqlorContext("pipeline") as sor:
existing = await sor.sqlExe( existing = await sor.sqlExe(
"SELECT id FROM pipeline_user_memory WHERE memory_key=${k}$ AND category=${c}$", "SELECT id FROM pipeline_user_memory WHERE memory_key=${k}$ AND category=${c}$ "
{"k": entry.key, "c": entry.category}, "AND scope=${s}$ AND scope_id=${sid}$",
{"k": entry.key, "c": entry.category, "s": entry.scope, "sid": entry.scope_id},
) )
if existing: if existing:
rid = existing[0].id rid = existing[0].id
@ -220,6 +259,8 @@ class MemoryStore:
"memory_key": entry.key, "memory_key": entry.key,
"content": entry.content, "content": entry.content,
"category": entry.category, "category": entry.category,
"scope": entry.scope,
"scope_id": entry.scope_id,
"priority": entry.priority, "priority": entry.priority,
}) })