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:
1 parent
8ec808b515
commit
5d9d420525
3 files changed
+865
-226
No files matched your search
+715
-209
File diff suppressed because it is too large.
Load diff
@@ -202,6 +202,28 @@ class KnowledgeDocument(Base):
|
||||
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):
|
||||
__tablename__ = "knowledge_graph_nodes"
|
||||
|
||||
|
||||
@@ -108,16 +108,29 @@ class PipelineScheduler:
|
||||
self._stop.wait(self.poll_interval)
|
||||
|
||||
def _process_trajectory(self, db, traj):
|
||||
"""处理单条轨迹:discovered -> splitting -> split_done -> indexed -> clustering -> done"""
|
||||
logger.info(f"开始处理轨迹 {traj.id},用户 {self.user_id}")
|
||||
"""处理单条轨迹 — Stage D: LLM智能抽取替代mock"""
|
||||
logger.info("开始处理轨迹 {},用户 {}".format(traj.id, self.user_id))
|
||||
|
||||
# step 1: discovered -> splitting
|
||||
traj.status = "splitting"
|
||||
db.commit()
|
||||
|
||||
# 模拟AtomTask拆分:生成2-5个模拟atom
|
||||
time.sleep(random.uniform(0.1, 0.5))
|
||||
atom_count = random.randint(2, 5)
|
||||
# Stage D: 使用对话内容进行真实分析
|
||||
# 从关联的对话中提取消息内容
|
||||
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.tasks_extracted = atom_count
|
||||
db.commit()
|
||||
@@ -126,28 +139,74 @@ class PipelineScheduler:
|
||||
traj.status = "split_done"
|
||||
db.commit()
|
||||
|
||||
# 为每个atom创建SkillTrajectoryLink
|
||||
for i in range(atom_count):
|
||||
# 为每个抽取的atom创建SkillTrajectoryLink
|
||||
for i, task in enumerate(atom_tasks):
|
||||
link = SkillTrajectoryLink(
|
||||
skill_name="__pending__",
|
||||
skill_name=task.get("skill_name", "__pending__"),
|
||||
user_id=self.user_id,
|
||||
trajectory_id=traj.id,
|
||||
atom_id=f"{traj.id}_atom_{i}",
|
||||
weightscore=random.randint(1, 10)
|
||||
atom_id="{}_atom_{}".format(traj.id[:8], i),
|
||||
weightscore=task.get("score", 5)
|
||||
)
|
||||
db.add(link)
|
||||
db.commit()
|
||||
|
||||
# step 3: split_done -> indexed (模拟向量索引)
|
||||
time.sleep(random.uniform(0.1, 0.3))
|
||||
# step 3: split_done -> indexed
|
||||
traj.status = "indexed"
|
||||
db.commit()
|
||||
|
||||
# step 4: indexed -> clustering (模拟聚类)
|
||||
time.sleep(random.uniform(0.1, 0.3))
|
||||
# step 4: indexed -> clustering
|
||||
traj.status = "clustering"
|
||||
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
|
||||
# 检查是否有同名skill,有则归属,无则创建新baby skill
|
||||
existing_skill = db.query(EvolutionSkill).filter(
|
||||
@@ -422,7 +481,7 @@ class EvolutionService:
|
||||
return result
|
||||
|
||||
def update_skill(self, skill_name: str, data: dict) -> Optional[dict]:
|
||||
"""编辑技能"""
|
||||
"""编辑技能 — 同时创建版本快照和写入SKILL.md"""
|
||||
skill = self.db.query(EvolutionSkill).filter(
|
||||
EvolutionSkill.user_id == self.user_id,
|
||||
EvolutionSkill.skill_name == skill_name
|
||||
@@ -430,6 +489,8 @@ class EvolutionService:
|
||||
if not skill:
|
||||
return None
|
||||
|
||||
version_before = skill.version
|
||||
|
||||
for key, value in data.items():
|
||||
if hasattr(skill, key) and key not in ('id', 'user_id', 'created_at'):
|
||||
setattr(skill, key, value)
|
||||
@@ -437,14 +498,64 @@ class EvolutionService:
|
||||
skill.version += 1
|
||||
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} 已更新",
|
||||
version_before=skill.version - 1, version_after=skill.version)
|
||||
self._add_event(skill_name, "edited", "技能 {} 已更新至v{}".format(skill_name, skill.version),
|
||||
version_before=version_before, version_after=skill.version)
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(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:
|
||||
"""删除技能"""
|
||||
skill = self.db.query(EvolutionSkill).filter(
|
||||
|
||||
Reference in new issue
Block a user