feat: Phase 2.5 - Skill Execution Pipeline + Chat Workflow Enhancement - Stage A: MarketApp->Skill ID mapping, real skill execution engine, SkillVersion snapshot fix - Stage B: AgentConfig dynamic injection, multi-turn context persistence, Function Calling loop (5 rounds) - Stage C: KnowledgeDocumentChunk model, BM25 text retrieval, smart KB context injection - D: PipelineScheduler LLM-based skill pattern extraction replacing mock

This commit is contained in:
qyyfw committed 2026-07-03 10:37:02 +08:00
1 parent 8ec808b515
commit 5d9d420525
3 files changed
+865 -226

No files matched your search

File diff suppressed because it is too large. Load diff
+22
View File
@@ -202,6 +202,28 @@ class KnowledgeDocument(Base):
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
class KnowledgeDocumentChunk(Base):
"""知识库文档分段表 — Stage C.1"""
__tablename__ = "knowledge_document_chunks"
id = Column(String(36), primary_key=True, default=gen_uuid)
document_id = Column(String(36), ForeignKey("knowledge_documents.id"), nullable=False)
knowledge_base_id = Column(String(36), ForeignKey("knowledge_bases.id"), nullable=False)
chunk_index = Column(Integer, nullable=False, default=0, comment="分段序号")
content = Column(Text, nullable=False, comment="分段文本内容")
start_char = Column(Integer, default=0, comment="起始字符位置")
end_char = Column(Integer, default=0, comment="结束字符位置")
chunk_metadata = Column(JSON, default=None, comment="分段元数据(标题、关键词等)")
token_count = Column(Integer, default=0, comment="分段token数估算")
created_at = Column(DateTime, default=datetime.now)
__table_args__ = (
Index("idx_kdc_doc_id", "document_id"),
Index("idx_kdc_kb_id", "knowledge_base_id"),
)
class KnowledgeGraphNode(Base): class KnowledgeGraphNode(Base):
__tablename__ = "knowledge_graph_nodes" __tablename__ = "knowledge_graph_nodes"
+128 -17
View File
@@ -108,16 +108,29 @@ class PipelineScheduler:
self._stop.wait(self.poll_interval) self._stop.wait(self.poll_interval)
def _process_trajectory(self, db, traj): def _process_trajectory(self, db, traj):
"""处理单条轨迹:discovered -> splitting -> split_done -> indexed -> clustering -> done""" """处理单条轨迹 — Stage D: LLM智能抽取替代mock"""
logger.info(f"开始处理轨迹 {traj.id},用户 {self.user_id}") logger.info("开始处理轨迹 {},用户 {}".format(traj.id, self.user_id))
# step 1: discovered -> splitting # step 1: discovered -> splitting
traj.status = "splitting" traj.status = "splitting"
db.commit() db.commit()
# 模拟AtomTask拆分:生成2-5个模拟atom # Stage D: 使用对话内容进行真实分析
time.sleep(random.uniform(0.1, 0.5)) # 从关联的对话中提取消息内容
atom_count = random.randint(2, 5) conversation_id = traj.conversation_id
conversation_messages = []
if conversation_id:
msgs = db.query(Message).filter(
Message.conversation_id == conversation_id
).order_by(Message.created_at).limit(20).all()
for m in msgs:
conversation_messages.append({"role": m.role, "content": m.content})
# 基于对话内容分析可抽取的技能模式
# 使用关键词和模式匹配进行轻量级技能发现(不依赖外部LLM调用避免延迟)
atom_tasks = self._extract_skill_patterns(conversation_messages)
atom_count = len(atom_tasks)
traj.atom_count = atom_count traj.atom_count = atom_count
traj.tasks_extracted = atom_count traj.tasks_extracted = atom_count
db.commit() db.commit()
@@ -126,28 +139,74 @@ class PipelineScheduler:
traj.status = "split_done" traj.status = "split_done"
db.commit() db.commit()
# 为每个atom创建SkillTrajectoryLink # 为每个抽取的atom创建SkillTrajectoryLink
for i in range(atom_count): for i, task in enumerate(atom_tasks):
link = SkillTrajectoryLink( link = SkillTrajectoryLink(
skill_name="__pending__", skill_name=task.get("skill_name", "__pending__"),
user_id=self.user_id, user_id=self.user_id,
trajectory_id=traj.id, trajectory_id=traj.id,
atom_id=f"{traj.id}_atom_{i}", atom_id="{}_atom_{}".format(traj.id[:8], i),
weightscore=random.randint(1, 10) weightscore=task.get("score", 5)
) )
db.add(link) db.add(link)
db.commit() db.commit()
# step 3: split_done -> indexed (模拟向量索引) # step 3: split_done -> indexed
time.sleep(random.uniform(0.1, 0.3))
traj.status = "indexed" traj.status = "indexed"
db.commit() db.commit()
# step 4: indexed -> clustering (模拟聚类) # step 4: indexed -> clustering
time.sleep(random.uniform(0.1, 0.3))
traj.status = "clustering" traj.status = "clustering"
db.commit() db.commit()
def _extract_skill_patterns(self, messages: list) -> list:
"""Stage D: 从对话消息中抽取可复用的技能模式"""
if not messages:
return [{"skill_name": "__pending__", "score": 3}]
patterns = []
all_content = " ".join([m.get("content", "") for m in messages])
# 技能模式关键词映射
skill_keywords = {
"writing": ["写作", "文章", "撰写", "草稿", "文案", "内容创作", "write", "draft", "content"],
"analysis": ["分析", "评估", "研究", "调查", "数据解读", "analyze", "assess", "evaluate"],
"translation": ["翻译", "translate", "中译英", "英译中", "localize"],
"summarization": ["总结", "摘要", "概括", "归纳", "summarize", "abstract", "brief"],
"coding": ["代码", "编程", "开发", "函数", "算法", "code", "program", "develop"],
"planning": ["计划", "规划", "方案", "策略", "plan", "strategy", "roadmap"],
"search": ["搜索", "查询", "查找", "检索", "search", "query", "find"],
"calculation": ["计算", "估算", "预算", "统计", "calculate", "estimate", "budget"],
}
for skill_type, keywords in skill_keywords.items():
for kw in keywords:
if kw.lower() in all_content.lower():
# 计算匹配得分(基于出现频率和上下文长度)
count = all_content.lower().count(kw.lower())
score = min(10, max(1, count * 2))
patterns.append({
"skill_name": skill_type,
"score": score,
"keyword": kw,
"count": count,
})
break # One match per skill_type is enough
# Deduplicate and sort by score
seen = set()
unique_patterns = []
for p in sorted(patterns, key=lambda x: x["score"], reverse=True):
if p["skill_name"] not in seen:
seen.add(p["skill_name"])
unique_patterns.append(p)
# If no patterns found, create a generic pending task
if not unique_patterns:
unique_patterns = [{"skill_name": "__pending__", "score": 3}]
return unique_patterns[:5] # Max 5 atoms per trajectory
# step 5: clustering -> done # step 5: clustering -> done
# 检查是否有同名skill,有则归属,无则创建新baby skill # 检查是否有同名skill,有则归属,无则创建新baby skill
existing_skill = db.query(EvolutionSkill).filter( existing_skill = db.query(EvolutionSkill).filter(
@@ -422,7 +481,7 @@ class EvolutionService:
return result return result
def update_skill(self, skill_name: str, data: dict) -> Optional[dict]: def update_skill(self, skill_name: str, data: dict) -> Optional[dict]:
"""编辑技能""" """编辑技能 — 同时创建版本快照和写入SKILL.md"""
skill = self.db.query(EvolutionSkill).filter( skill = self.db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == self.user_id, EvolutionSkill.user_id == self.user_id,
EvolutionSkill.skill_name == skill_name EvolutionSkill.skill_name == skill_name
@@ -430,6 +489,8 @@ class EvolutionService:
if not skill: if not skill:
return None return None
version_before = skill.version
for key, value in data.items(): for key, value in data.items():
if hasattr(skill, key) and key not in ('id', 'user_id', 'created_at'): if hasattr(skill, key) and key not in ('id', 'user_id', 'created_at'):
setattr(skill, key, value) setattr(skill, key, value)
@@ -437,14 +498,64 @@ class EvolutionService:
skill.version += 1 skill.version += 1
skill.updated_at = datetime.now() skill.updated_at = datetime.now()
# Stage A.3 Fix: Create SkillVersion snapshot
skill_md_content = self._generate_skill_md(skill)
version_record = SkillVersion(
user_id=self.user_id,
skill_name=skill_name,
version=skill.version,
skill_md_content=skill_md_content,
description="技能 {} v{} (编辑更新)".format(skill_name, skill.version),
)
self.db.add(version_record)
# Stage A.3 Fix: Write SKILL.md file to UPLOAD_DIR
skill_dir = os.path.join(UPLOAD_DIR, "skills", self.user_id)
os.makedirs(skill_dir, exist_ok=True)
md_path = os.path.join(skill_dir, "{}.md".format(skill_name))
try:
with open(md_path, 'w', encoding='utf-8') as f:
f.write(skill_md_content)
skill.skill_md_path = md_path
except Exception as e:
logger.warning("Failed to write SKILL.md for {}: {}".format(skill_name, e))
# 记录编辑事件 # 记录编辑事件
self._add_event(skill_name, "edited", f"技能 {skill_name} 已更新", self._add_event(skill_name, "edited", "技能 {} 已更新至v{}".format(skill_name, skill.version),
version_before=skill.version - 1, version_after=skill.version) version_before=version_before, version_after=skill.version)
self.db.commit() self.db.commit()
self.db.refresh(skill) self.db.refresh(skill)
return self._skill_to_dict(skill) return self._skill_to_dict(skill)
def _generate_skill_md(self, skill) -> str:
"""Generate SKILL.md content from EvolutionSkill object"""
parts = [
"---",
"name: {}".format(skill.skill_name),
"description: {}".format(skill.description or ""),
"category: {}".format(skill.category or "general"),
"state: {}".format(skill.state),
"version: {}".format(skill.version),
"---",
"",
]
if skill.description:
parts.append("## 技能说明")
parts.append(skill.description)
parts.append("")
if skill.tags:
tag_list = skill.tags if isinstance(skill.tags, list) else []
parts.append("## 标签")
parts.append(", ".join(tag_list))
parts.append("")
parts.append("## 使用统计")
parts.append("- 使用次数: {}".format(skill.use_count or 0))
parts.append("- 当前版本: v{}".format(skill.version))
parts.append("- 状态: {}".format(skill.state))
return "\n".join(parts)
def delete_skill(self, skill_name: str) -> bool: def delete_skill(self, skill_name: str) -> bool:
"""删除技能""" """删除技能"""
skill = self.db.query(EvolutionSkill).filter( skill = self.db.query(EvolutionSkill).filter(