bugfix
This commit is contained in:
parent
7fbf7f7a31
commit
6dda573aed
@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from typing import Literal
|
||||
from appPublic.worker import awaitify
|
||||
from appPublic.streamhttpclient import StreamHttpClient, liner
|
||||
from .skillkit_wrapper import SkillkitWrapper
|
||||
|
||||
# ---------------------------
|
||||
@ -12,32 +13,32 @@ from .skillkit_wrapper import SkillkitWrapper
|
||||
# ---------------------------
|
||||
@dataclass
|
||||
class SkillDecision:
|
||||
skill: str
|
||||
params: dict
|
||||
reason: Optional[str] = None
|
||||
skill: str
|
||||
params: dict
|
||||
reason: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class PlanState:
|
||||
user_intent: str
|
||||
skill: str
|
||||
user_intent: str
|
||||
skill: str
|
||||
script: str
|
||||
params: dict
|
||||
missing: List[str] = field(default_factory=list)
|
||||
params: dict
|
||||
missing: List[str] = field(default_factory=list)
|
||||
|
||||
# ---------------------------
|
||||
# 自定义异常
|
||||
# ---------------------------
|
||||
class MissingParams(Exception):
|
||||
def __init__(self, skill: str, fields: List[str]):
|
||||
self.skill = skill
|
||||
self.fields = fields
|
||||
def __init__(self, skill: str, fields: List[str]):
|
||||
self.skill = skill
|
||||
self.fields = fields
|
||||
|
||||
# ---------------------------
|
||||
# LLM 接口(可替换为你的模型)
|
||||
# ---------------------------
|
||||
class LLM:
|
||||
async def complete(self, prompt: str) -> str:
|
||||
raise NotImplementedError
|
||||
async def complete(self, prompt: str) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
# ---------------------------
|
||||
# DummyLLM 示例(测试用)
|
||||
@ -45,38 +46,39 @@ class LLM:
|
||||
class DummyLLM(LLM):
|
||||
def __init__(self, llmid, apikey):
|
||||
self.llmid = llmid
|
||||
self.akikey = apikey
|
||||
self.apikey = apikey
|
||||
|
||||
async def complete(self, prompt: str) -> str:
|
||||
async def complete(self, prompt: str) -> str:
|
||||
hc = StreamHttpClient()
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.apikey}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.apikey}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
d = {
|
||||
'llmid': self.llmid,
|
||||
'prompt': prompt
|
||||
}
|
||||
'llmid': self.llmid,
|
||||
'prompt': prompt
|
||||
}
|
||||
url = 'https://opencomputing.ai/v1/llm'
|
||||
reco = hc('POST', url, headers=headers, data=json.dumps(d))
|
||||
doc = ''
|
||||
async for chunk in liner(reco):
|
||||
try:
|
||||
d = json.loads(chunk)
|
||||
except Exception as e:
|
||||
print(f'****{chunk=} error {e} {format_exc()}')
|
||||
continue
|
||||
async for chunk in liner(reco):
|
||||
try:
|
||||
d = json.loads(chunk)
|
||||
except Exception as e:
|
||||
print(f'****{chunk=} error {e} {format_exc()}')
|
||||
continue
|
||||
if d.get('content'):
|
||||
doc = f'{doc}{d["content"]}'
|
||||
else:
|
||||
print(f'{f}:{d} error')
|
||||
return json.loads(doc)
|
||||
doc = f'{doc}{d["content"]}'
|
||||
else:
|
||||
print(f'{d} error')
|
||||
return doc
|
||||
|
||||
# ---------------------------
|
||||
# Agent 实现
|
||||
# ---------------------------
|
||||
class Agent:
|
||||
def __init__(self, llm: LLM, skillkit):
|
||||
self.llm = llm
|
||||
def __init__(self, llm: LLM, skillkit):
|
||||
self.llm = llm
|
||||
self.skillkit = skillkit
|
||||
self.skills = None
|
||||
self.loaded = False
|
||||
@ -88,30 +90,30 @@ class Agent:
|
||||
for s in self.skills:
|
||||
self.skillkit.load_skill(s.name)
|
||||
|
||||
# ---------------------------
|
||||
# plan: 多 skill 候选 + 参数抽取
|
||||
# ---------------------------
|
||||
async def plan(self, user_text: str):
|
||||
# ---------------------------
|
||||
# plan: 多 skill 候选 + 参数抽取
|
||||
# ---------------------------
|
||||
async def plan(self, user_text: str):
|
||||
self.load_skills()
|
||||
candidates = await self._candidate_skills(user_text)
|
||||
decision = await self._plan_with_candidates(user_text, candidates)
|
||||
try:
|
||||
validated_params = self._validate_params(decision)
|
||||
except MissingParams as e:
|
||||
question = await self._ask_user_for_params(user_text, decision.skill, e.fields)
|
||||
state = PlanState(
|
||||
user_intent=user_text,
|
||||
skill=decision.skill,
|
||||
candidates = await self._candidate_skills(user_text)
|
||||
decision = await self._plan_with_candidates(user_text, candidates)
|
||||
try:
|
||||
validated_params = self._validate_params(decision)
|
||||
except MissingParams as e:
|
||||
question = await self._ask_user_for_params(user_text, decision.skill, e.fields)
|
||||
state = PlanState(
|
||||
user_intent=user_text,
|
||||
skill=decision.skill,
|
||||
script=decision.script,
|
||||
params=decision.params,
|
||||
missing=e.fields
|
||||
)
|
||||
return {
|
||||
params=decision.params,
|
||||
missing=e.fields
|
||||
)
|
||||
return {
|
||||
"type": "clarification",
|
||||
"state": state,
|
||||
"question": question
|
||||
}
|
||||
return {
|
||||
return {
|
||||
"type": "script_call",
|
||||
"script": decision.script,
|
||||
"skill": decision.skill,
|
||||
@ -122,15 +124,15 @@ class Agent:
|
||||
def get_scripts(self, skillname):
|
||||
return self.skillkit.get_skill_scripts(skillname)
|
||||
|
||||
# ---------------------------
|
||||
# resume: 补 missing 参数
|
||||
# ---------------------------
|
||||
async def resume(self, state: PlanState, user_reply: str):
|
||||
skill_spec = next(s for s in self.skills if s.name == state.skill)
|
||||
schema_fields = next(s.params for s in skill.scripts if s.name==state.script)
|
||||
# ---------------------------
|
||||
# resume: 补 missing 参数
|
||||
# ---------------------------
|
||||
async def resume(self, state: PlanState, user_reply: str):
|
||||
skill_spec = next(s for s in self.skills if s.name == state.skill)
|
||||
schema_fields = self.skillkit.get_script_params(state.skill, state.script)
|
||||
if schema_fields is None:
|
||||
schema_fields = []
|
||||
prompt = f"""
|
||||
prompt = f"""
|
||||
You are an agent helping a user fill parameters for a skill.
|
||||
|
||||
Skill name: {state.skill}
|
||||
@ -155,35 +157,38 @@ Task:
|
||||
- All output must match the skill parameter schema.
|
||||
- Output JSON only with the missing parameters.
|
||||
"""
|
||||
raw = await self.llm.complete(prompt)
|
||||
new_params = json.loads(raw)
|
||||
raw = await self.llm.complete(prompt)
|
||||
new_params = json.loads(raw)
|
||||
|
||||
state.params.update(new_params)
|
||||
state.params.update(new_params)
|
||||
|
||||
# 校验 schema
|
||||
try:
|
||||
validated = self._validate_params(SkillDecision(skill=state.skill, params=state.params))
|
||||
except MissingParams as e:
|
||||
state.missing = e.fields
|
||||
question = await self._ask_user_for_params(state.user_intent, state.skill, e.fields)
|
||||
return {"type": "clarification", "state": state, "question": question}
|
||||
# 校验 schema
|
||||
try:
|
||||
validated = self._validate_params(SkillDecision(skill=state.skill, params=state.params))
|
||||
except MissingParams as e:
|
||||
state.missing = e.fields
|
||||
question = await self._ask_user_for_params(state.user_intent, state.skill, e.fields)
|
||||
return {"type": "clarification", "state": state, "question": question}
|
||||
|
||||
# 参数完整,返回可直接调用 skill
|
||||
return {"type": "skill_call", "skill": state.skill, "params": validated}
|
||||
# 参数完整,返回可直接调用 skill
|
||||
return {"type": "skill_call", "skill": state.skill, "params": validated}
|
||||
|
||||
# ---------------------------
|
||||
# 内部方法
|
||||
# ---------------------------
|
||||
# ---------------------------
|
||||
# 内部方法
|
||||
# ---------------------------
|
||||
|
||||
def scripts_info(self, skill):
|
||||
def scripts_info(self, skill_name):
|
||||
d = []
|
||||
skill = self.skillkit.load_skill(skill_name)
|
||||
for s in skill.scripts:
|
||||
d.append( f'name:{s.name}, description:{s.description}, params:{str(s.params}'
|
||||
return "Scripts: '::'.join(d)
|
||||
params = self.skillkit.get_script_params(skill_name, s.name)
|
||||
print(f'{params=}')
|
||||
d.append( f'name:{s.name}, description:{s.description}, params:{str(params)}')
|
||||
return "Scripts: " + '::'.join(d)
|
||||
|
||||
async def _candidate_skills(self, user_text: str):
|
||||
skill_list = "\n".join(f"- skillname:{s.name}({s.description}): {self.scripts_info(s)}" for s in self.skills)
|
||||
prompt = f"""
|
||||
async def _candidate_skills(self, user_text: str):
|
||||
skill_list = "\n".join(f"- skillname:{s.name}({s.description}): {self.scripts_info(s.name)}" for s in self.skills)
|
||||
prompt = f"""
|
||||
User request:
|
||||
\"\"\"{user_text}\"\"\"
|
||||
|
||||
@ -195,15 +200,15 @@ Select up to 3 most relevant skill's scripts.
|
||||
|
||||
Output JSON list only.
|
||||
"""
|
||||
raw = await self.llm.complete(prompt)
|
||||
return json.loads(raw)
|
||||
raw = await self.llm.complete(prompt)
|
||||
return json.loads(raw)
|
||||
|
||||
async def _plan_with_candidates(self, user_text: str, candidates: list[str]):
|
||||
specs = [s for s in self.skills if s.name in candidates]
|
||||
spec_desc = "\n".join(
|
||||
f"- {s.name}: inputs={list(s.schema.model_fields.keys())}" for s in specs if s.schema
|
||||
)
|
||||
prompt = f"""
|
||||
async def _plan_with_candidates(self, user_text: str, candidates: list[str]):
|
||||
specs = [s for s in self.skills if s.name in candidates]
|
||||
spec_desc = "\n".join(
|
||||
f"- {s.name}: inputs={list(s.schema.model_fields.keys())}" for s in specs if s.schema
|
||||
)
|
||||
prompt = f"""
|
||||
User request:
|
||||
\"\"\"{user_text}\"\"\"
|
||||
|
||||
@ -226,23 +231,23 @@ Output:
|
||||
"reason": "..."
|
||||
}}
|
||||
"""
|
||||
raw = await self.llm.complete(prompt)
|
||||
return SkillDecision(**json.loads(raw))
|
||||
raw = await self.llm.complete(prompt)
|
||||
return SkillDecision(**json.loads(raw))
|
||||
|
||||
def _validate_params(self, decision: SkillDecision):
|
||||
spec = next(s for s in self.skills if s.name == decision.skill)
|
||||
if not spec.schema:
|
||||
return decision.params
|
||||
try:
|
||||
return spec.schema(**decision.params).dict()
|
||||
except ValidationError as e:
|
||||
missing = [err["loc"][0] for err in e.errors() if err["type"] == "missing"]
|
||||
if missing:
|
||||
raise MissingParams(decision.skill, missing)
|
||||
raise
|
||||
def _validate_params(self, decision: SkillDecision):
|
||||
spec = next(s for s in self.skills if s.name == decision.skill)
|
||||
if not spec.schema:
|
||||
return decision.params
|
||||
try:
|
||||
return spec.schema(**decision.params).dict()
|
||||
except ValidationError as e:
|
||||
missing = [err["loc"][0] for err in e.errors() if err["type"] == "missing"]
|
||||
if missing:
|
||||
raise MissingParams(decision.skill, missing)
|
||||
raise
|
||||
|
||||
async def _ask_user_for_params(self, user_text: str, skill: str, fields: List[str]):
|
||||
prompt = f"""
|
||||
async def _ask_user_for_params(self, user_text: str, skill: str, fields: List[str]):
|
||||
prompt = f"""
|
||||
User request:
|
||||
\"\"\"{user_text}\"\"\"
|
||||
|
||||
@ -251,15 +256,15 @@ The script "{script} in skill "{skill}" requires the following missing parameter
|
||||
|
||||
Ask the user a concise clarification question.
|
||||
"""
|
||||
return await self.llm.complete(prompt)
|
||||
return await self.llm.complete(prompt)
|
||||
|
||||
# ---------------------------
|
||||
# 测试运行
|
||||
# ---------------------------
|
||||
async def skillagent(llm, apikey, user_skillroot, sys_skillroot):
|
||||
llm = DummyLLM('8L4hFJ4QpSMyu1UP03Juo', 'eYgNuD6sVQgbj-khOOUNU')
|
||||
skillkit = SkillKitWrapper(skill_rootpath)
|
||||
agent = Agent(llm, skillkit)
|
||||
async def skillagent(llm, apikey, user_skillroot, sys_skillroot=None):
|
||||
llm = DummyLLM('8L4hFJ4QpSMyu1UP03Juo', 'eYgNuD6sVQgbj-khOOUNU')
|
||||
skillkit = SkillkitWrapper(user_skillroot)
|
||||
agent = Agent(llm, skillkit)
|
||||
|
||||
while True:
|
||||
print('What you want to do?')
|
||||
|
||||
@ -3,6 +3,7 @@ from skillkit import SkillManager
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from appPublic.dictObject import DictObject
|
||||
|
||||
def find_missing_params(
|
||||
input_schema: Dict[str, Any],
|
||||
@ -19,7 +20,7 @@ def find_missing_params(
|
||||
|
||||
return missing
|
||||
|
||||
def load_schemas(yaml_path: str) -> Dict[str, Any]:
|
||||
def load_schemas(path) -> Dict[str, Any]:
|
||||
"""
|
||||
从 YAML 文件中读取 script 输入参数定义
|
||||
|
||||
@ -32,47 +33,46 @@ def load_schemas(yaml_path: str) -> Dict[str, Any]:
|
||||
}
|
||||
}
|
||||
"""
|
||||
path = Path(yaml_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Script yaml not found: {yaml_path}")
|
||||
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
if "script" not in data or "inputs" not in data:
|
||||
raise ValueError("Invalid script yaml format")
|
||||
|
||||
return {
|
||||
"script": data["script"],
|
||||
"description": data.get("description", ""),
|
||||
"inputs": data["inputs"],
|
||||
}
|
||||
return DictObject(data)
|
||||
|
||||
class SkillkitWrapper:
|
||||
def __init__(self, user_skillsroot, sys_skillsroot=None):
|
||||
|
||||
self.client = SkillManager(project_skill_dir=skillroot,
|
||||
self.client = SkillManager(project_skill_dir=user_skillsroot,
|
||||
anthropic_config_dir=sys_skillsroot)
|
||||
self.client.discover()
|
||||
self.schemas = {}
|
||||
|
||||
def list_skills(self):
|
||||
return self.client.list_skills()
|
||||
|
||||
def load_skill(self, skillname):
|
||||
def load_skill(self, skill_name):
|
||||
skill = self.client.load_skill(skill_name)
|
||||
if not hasattr(skill, 'schemas'):
|
||||
fp = os.path.join(skill.base_dir, 'schemas.yaml')
|
||||
if os.path.exists(fp):
|
||||
data = load_schema(fp)
|
||||
skill.schemas = data
|
||||
for s in skill.scripts:
|
||||
s.params = next(sch.inputs for sch in skill.schemas if sch.script==script_name)
|
||||
print(skill, dir(skill))
|
||||
schemaspath = skill.base_directory / 'schemas.yaml'
|
||||
if schemaspath.exists():
|
||||
if not self.schemas.get(skill_name):
|
||||
data = load_schemas(schemaspath)
|
||||
self.schemas[skill_name] = data
|
||||
print(f'{data=}, {str(schemaspath)}')
|
||||
|
||||
return skill
|
||||
|
||||
def get_script_params(self, skill_name, script_name):
|
||||
skill = self.load_skill(skill_name)
|
||||
return next(s.params for s in skill.scripts if s.name==script_name)
|
||||
d = self.schemas.get('skill_name')
|
||||
if not d:
|
||||
return []
|
||||
m = d.scripts.get(script_name)
|
||||
if not m:
|
||||
return []
|
||||
return m.inputs
|
||||
|
||||
def get_skill_scripts(self, skill_name):
|
||||
skill = self.load_skill(skill_name)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user