agent-assembler 0.2.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_assembler/__init__.py +7 -0
- agent_assembler/adapters/__init__.py +5 -0
- agent_assembler/adapters/base.py +23 -0
- agent_assembler/adapters/coze.py +91 -0
- agent_assembler/adapters/qianwen.py +98 -0
- agent_assembler/assembler.py +85 -0
- agent_assembler/deploy/__init__.py +3 -0
- agent_assembler/deploy/coze_api.py +95 -0
- agent_assembler/deploy/qianwen_api.py +58 -0
- agent_assembler/recipe.py +66 -0
- agent_assembler-0.2.0.dev1.dist-info/METADATA +51 -0
- agent_assembler-0.2.0.dev1.dist-info/RECORD +15 -0
- agent_assembler-0.2.0.dev1.dist-info/WHEEL +5 -0
- agent_assembler-0.2.0.dev1.dist-info/licenses/LICENSE +22 -0
- agent_assembler-0.2.0.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
from ..recipe import Recipe
|
|
5
|
+
|
|
6
|
+
class BaseAdapter(ABC):
|
|
7
|
+
"""Base class for all platform adapters."""
|
|
8
|
+
|
|
9
|
+
PLATFORM_NAME = "Base"
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def export(self, recipe: Recipe) -> Dict[str, Any]:
|
|
13
|
+
"""Export recipe to target platform format."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def validate(self, recipe: Recipe) -> List[str]:
|
|
18
|
+
"""Validate recipe against platform constraints."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def deploy(self, recipe: Recipe) -> bool:
|
|
22
|
+
"""Deploy agent to platform."""
|
|
23
|
+
raise NotImplementedError("Deploy not implemented.")
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from ..recipe import Recipe
|
|
3
|
+
from .base import BaseAdapter
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CozeAdapter(BaseAdapter):
|
|
8
|
+
"""Map Agent Assembler Recipe to Coze Bot Configuration.
|
|
9
|
+
|
|
10
|
+
Coze Bots require:
|
|
11
|
+
- bot_info.name
|
|
12
|
+
- bot_info.description
|
|
13
|
+
- bot_info.prompt_info.prompt (system instruction)
|
|
14
|
+
- bot_info.onboarding_info.prologue (welcome message)
|
|
15
|
+
|
|
16
|
+
This adapter injects Recipe + Skills content into the Coze DSL format.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
PLATFORM_NAME = "Coze"
|
|
20
|
+
|
|
21
|
+
def __init__(self, skills_dir: Optional[str] = None):
|
|
22
|
+
"""Initialize with optional skills directory for JIT loading."""
|
|
23
|
+
self.skills_dir = skills_dir
|
|
24
|
+
|
|
25
|
+
def _load_skills(self, recipe: Recipe) -> str:
|
|
26
|
+
"""Load skill contents and format as prompt section."""
|
|
27
|
+
if not self.skills_dir:
|
|
28
|
+
return "(Skills directory not configured)"
|
|
29
|
+
|
|
30
|
+
sections = []
|
|
31
|
+
for skill_name in recipe.skills:
|
|
32
|
+
skill_path = os.path.join(self.skills_dir, skill_name, "SKILL.md")
|
|
33
|
+
if os.path.exists(skill_path):
|
|
34
|
+
with open(skill_path, "r", encoding="utf-8") as f:
|
|
35
|
+
sections.append(f"### Skill: {skill_name}\n{f.read()}")
|
|
36
|
+
else:
|
|
37
|
+
sections.append(f"### Skill: {skill_name}\n[Warning: Skill file not found]")
|
|
38
|
+
|
|
39
|
+
return "\n\n".join(sections)
|
|
40
|
+
|
|
41
|
+
def export(self, recipe: Recipe) -> Dict[str, Any]:
|
|
42
|
+
"""Export recipe as Coze-compatible bot configuration."""
|
|
43
|
+
# Build system prompt
|
|
44
|
+
system_prompt = f"# Role\nYou are an AI assistant specialized in **{recipe.name}**.\n\n"
|
|
45
|
+
|
|
46
|
+
if recipe.notes:
|
|
47
|
+
system_prompt += f"## Context\n{recipe.notes}\n\n"
|
|
48
|
+
|
|
49
|
+
system_prompt += "## Skills & Instructions\n"
|
|
50
|
+
skills_content = self._load_skills(recipe)
|
|
51
|
+
system_prompt += skills_content
|
|
52
|
+
|
|
53
|
+
system_prompt += "\n\n## Execution Rules\n"
|
|
54
|
+
system_prompt += "- Follow the skill instructions strictly.\n"
|
|
55
|
+
system_prompt += "- If user query doesn\'t match any skill, fall back to general assistance.\n"
|
|
56
|
+
|
|
57
|
+
# Construct Coze DSL
|
|
58
|
+
bot_config = {
|
|
59
|
+
"bot_info": {
|
|
60
|
+
"name": recipe.name,
|
|
61
|
+
"description": recipe.notes or f"Agent for {recipe.name}",
|
|
62
|
+
"prompt_info": {
|
|
63
|
+
"prompt": system_prompt
|
|
64
|
+
},
|
|
65
|
+
"onboarding_info": {
|
|
66
|
+
"prologue": f"你好,我是{recipe.name}助手。请告诉我你需要什么帮助?"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"model_info": {
|
|
70
|
+
"model_name": "coze-pro"
|
|
71
|
+
},
|
|
72
|
+
"metadata": {
|
|
73
|
+
"source": "agent-assembler",
|
|
74
|
+
"recipe_name": recipe.name,
|
|
75
|
+
"trigger_keywords": recipe.trigger_keywords,
|
|
76
|
+
"skills_count": len(recipe.skills)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return bot_config
|
|
81
|
+
|
|
82
|
+
def validate(self, recipe: Recipe) -> List[str]:
|
|
83
|
+
"""Validate recipe against Coze platform constraints."""
|
|
84
|
+
errors = []
|
|
85
|
+
if len(recipe.name) > 50:
|
|
86
|
+
errors.append(f"Bot name \'{recipe.name}\' exceeds 50 characters limit.")
|
|
87
|
+
if not recipe.name:
|
|
88
|
+
errors.append("Bot name cannot be empty.")
|
|
89
|
+
if len(recipe.notes) > 500:
|
|
90
|
+
errors.append("Bot description exceeds 500 characters limit.")
|
|
91
|
+
return errors
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from ..recipe import Recipe
|
|
3
|
+
from .base import BaseAdapter
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class QianwenAdapter(BaseAdapter):
|
|
8
|
+
"""Map Agent Assembler Recipe to Qianwen (Tongyi) Agent Format.
|
|
9
|
+
|
|
10
|
+
Qianwen agents (百炼/通义) require:
|
|
11
|
+
- name: Agent name
|
|
12
|
+
- description: Brief description
|
|
13
|
+
- system_prompt: Core instructions
|
|
14
|
+
- model: LLM model selection
|
|
15
|
+
|
|
16
|
+
This adapter generates Chinese-optimized prompts.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
PLATFORM_NAME = "Qianwen"
|
|
20
|
+
|
|
21
|
+
def __init__(self, skills_dir: Optional[str] = None):
|
|
22
|
+
"""Initialize with optional skills directory for JIT loading."""
|
|
23
|
+
self.skills_dir = skills_dir
|
|
24
|
+
|
|
25
|
+
def _load_skills(self, recipe: Recipe) -> str:
|
|
26
|
+
"""Load skill contents and format as prompt section."""
|
|
27
|
+
if not self.skills_dir:
|
|
28
|
+
return "(未配置技能目录)"
|
|
29
|
+
|
|
30
|
+
sections = []
|
|
31
|
+
for skill_name in recipe.skills:
|
|
32
|
+
skill_path = os.path.join(self.skills_dir, skill_name, "SKILL.md")
|
|
33
|
+
if os.path.exists(skill_path):
|
|
34
|
+
with open(skill_path, "r", encoding="utf-8") as f:
|
|
35
|
+
sections.append(f"### 技能: {skill_name}\n{f.read()}")
|
|
36
|
+
else:
|
|
37
|
+
sections.append(f"### 技能: {skill_name}\n[警告: 技能文件未找到]")
|
|
38
|
+
|
|
39
|
+
return "\n\n".join(sections)
|
|
40
|
+
|
|
41
|
+
def export(self, recipe: Recipe) -> Dict[str, Any]:
|
|
42
|
+
"""Export recipe as Qianwen-compatible agent configuration."""
|
|
43
|
+
# Build system prompt in Chinese
|
|
44
|
+
system_prompt = f"# 角色设定\n你是一个专业的 **{recipe.name}** 助手。\n\n"
|
|
45
|
+
|
|
46
|
+
if recipe.notes:
|
|
47
|
+
system_prompt += f"## 背景信息\n{recipe.notes}\n\n"
|
|
48
|
+
|
|
49
|
+
system_prompt += "## 可用技能与指令\n"
|
|
50
|
+
skills_content = self._load_skills(recipe)
|
|
51
|
+
system_prompt += skills_content
|
|
52
|
+
|
|
53
|
+
system_prompt += "\n\n## 执行规则\n"
|
|
54
|
+
system_prompt += "- 严格按照技能指令执行任务\n"
|
|
55
|
+
system_prompt += "- 如果用户请求不匹配任何技能,提供通用帮助\n"
|
|
56
|
+
system_prompt += "- 保持回答简洁、专业、准确\n"
|
|
57
|
+
|
|
58
|
+
# Construct Qianwen DSL
|
|
59
|
+
agent_config = {
|
|
60
|
+
"name": recipe.name,
|
|
61
|
+
"description": recipe.notes or f"智能体: {recipe.name}",
|
|
62
|
+
"system_prompt": system_prompt,
|
|
63
|
+
"welcome_message": f"你好,我是{recipe.name}助手,请问有什么可以帮您?",
|
|
64
|
+
"model": "qwen-max",
|
|
65
|
+
"parameters": {
|
|
66
|
+
"temperature": 0.7,
|
|
67
|
+
"top_p": 0.8,
|
|
68
|
+
"max_tokens": 2000
|
|
69
|
+
},
|
|
70
|
+
"metadata": {
|
|
71
|
+
"source": "agent-assembler",
|
|
72
|
+
"recipe_name": recipe.name,
|
|
73
|
+
"trigger_keywords": recipe.trigger_keywords,
|
|
74
|
+
"skills_count": len(recipe.skills),
|
|
75
|
+
"routing": recipe.routing
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return agent_config
|
|
80
|
+
|
|
81
|
+
def validate(self, recipe: Recipe) -> List[str]:
|
|
82
|
+
"""Validate recipe against Qianwen platform constraints."""
|
|
83
|
+
errors = []
|
|
84
|
+
if len(recipe.name) > 30:
|
|
85
|
+
errors.append(f"智能体名称 \'{recipe.name}\' 超过 30 字符限制。")
|
|
86
|
+
if not recipe.name:
|
|
87
|
+
errors.append("智能体名称不能为空。")
|
|
88
|
+
# Check for valid routing if specified
|
|
89
|
+
valid_routings = [
|
|
90
|
+
None, "engineering-stage-agent", "finance-agent",
|
|
91
|
+
"operations-venue-agent", "project-pmo-agent",
|
|
92
|
+
"digital-tech-agent", "planning-agent",
|
|
93
|
+
"marketing-promotion-agent", "legal-agent",
|
|
94
|
+
"agriculture-agent", "guandan-agent", "gr-agent", "media-agent"
|
|
95
|
+
]
|
|
96
|
+
if recipe.routing and recipe.routing not in valid_routings:
|
|
97
|
+
errors.append(f"路由目标 \'{recipe.routing}\' 不在有效 Agent 列表中。")
|
|
98
|
+
return errors
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import List, Optional, Dict
|
|
6
|
+
from .recipe import Recipe
|
|
7
|
+
|
|
8
|
+
class Assembler:
|
|
9
|
+
"""The core engine of Agent Assembler."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, recipes_dir: str, skills_dir: str):
|
|
12
|
+
self.recipes_dir = os.path.abspath(recipes_dir)
|
|
13
|
+
self.skills_dir = os.path.abspath(skills_dir)
|
|
14
|
+
self.recipes: List[Recipe] = []
|
|
15
|
+
|
|
16
|
+
self._load_recipes()
|
|
17
|
+
|
|
18
|
+
def _load_recipes(self):
|
|
19
|
+
"""Load all recipes from the configured directory."""
|
|
20
|
+
if not os.path.exists(self.recipes_dir):
|
|
21
|
+
raise FileNotFoundError(f"Recipes directory not found: {self.recipes_dir}")
|
|
22
|
+
|
|
23
|
+
for root, _, files in os.walk(self.recipes_dir):
|
|
24
|
+
for file in files:
|
|
25
|
+
if file.endswith('.json'):
|
|
26
|
+
path = os.path.join(root, file)
|
|
27
|
+
try:
|
|
28
|
+
recipe = Recipe.from_json(path)
|
|
29
|
+
# Pre-load skills? No, JIT load them when matched to save time/memory.
|
|
30
|
+
# But for SDK v1, maybe eager load is fine for small datasets.
|
|
31
|
+
# Let's do lazy loading in assemble() for better performance.
|
|
32
|
+
self.recipes.append(recipe)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"Error loading recipe {path}: {e}")
|
|
35
|
+
|
|
36
|
+
def match_recipe(self, query: str) -> Optional[Recipe]:
|
|
37
|
+
"""Find the best matching recipe for a user query."""
|
|
38
|
+
query_lower = query.lower()
|
|
39
|
+
matches = []
|
|
40
|
+
|
|
41
|
+
for recipe in self.recipes:
|
|
42
|
+
for keyword in recipe.trigger_keywords:
|
|
43
|
+
if keyword.lower() in query_lower:
|
|
44
|
+
matches.append(recipe)
|
|
45
|
+
break # Found a match for this recipe
|
|
46
|
+
|
|
47
|
+
if not matches:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
# Return the first match (can be improved with scoring later)
|
|
51
|
+
return matches[0]
|
|
52
|
+
|
|
53
|
+
def assemble(self, query: str) -> Dict:
|
|
54
|
+
"""Perform JIT assembly."""
|
|
55
|
+
recipe = self.match_recipe(query)
|
|
56
|
+
|
|
57
|
+
if not recipe:
|
|
58
|
+
return {
|
|
59
|
+
"status": "fallback",
|
|
60
|
+
"message": "No matching recipe found.",
|
|
61
|
+
"system_prompt": query # Pass through
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Load skills JIT
|
|
65
|
+
recipe.load_skills(self.skills_dir)
|
|
66
|
+
|
|
67
|
+
# Construct Prompt
|
|
68
|
+
system_prompt = f"# Role\nYou are an AI assistant executing the task: {recipe.name}\n\n"
|
|
69
|
+
|
|
70
|
+
if recipe.notes:
|
|
71
|
+
system_prompt += f"## Context\n{recipe.notes}\n\n"
|
|
72
|
+
|
|
73
|
+
system_prompt += "## Skills & Rules\n"
|
|
74
|
+
|
|
75
|
+
for skill_ref in recipe.skill_refs:
|
|
76
|
+
system_prompt += f"### Skill: {skill_ref.name}\n{skill_ref.content}\n\n"
|
|
77
|
+
|
|
78
|
+
system_prompt += f"## User Query\n{query}"
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"status": "success",
|
|
82
|
+
"recipe": recipe.name,
|
|
83
|
+
"skills_loaded": [s.name for s in recipe.skill_refs],
|
|
84
|
+
"system_prompt": system_prompt
|
|
85
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any, List, Optional
|
|
5
|
+
|
|
6
|
+
class CozeApiClient:
|
|
7
|
+
# China Domestic API
|
|
8
|
+
API_BASE = "https://api.coze.cn/v1"
|
|
9
|
+
|
|
10
|
+
def __init__(self, token: str):
|
|
11
|
+
self.headers = {
|
|
12
|
+
"Authorization": f"Bearer {token}",
|
|
13
|
+
"Content-Type": "application/json"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def create_bot(self, name: str, description: str, prompt: str, space_id: str) -> Optional[str]:
|
|
17
|
+
"""
|
|
18
|
+
Create a bot on Coze (v1 API).
|
|
19
|
+
Returns bot_id if success.
|
|
20
|
+
"""
|
|
21
|
+
url = f"{self.API_BASE}/bot/create"
|
|
22
|
+
payload = {
|
|
23
|
+
"space_id": space_id,
|
|
24
|
+
"name": name,
|
|
25
|
+
"description": description,
|
|
26
|
+
"prompt_info": {
|
|
27
|
+
"prompt": prompt
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
resp = requests.post(url, headers=self.headers, json=payload)
|
|
33
|
+
data = resp.json()
|
|
34
|
+
if data.get("code") == 0:
|
|
35
|
+
bot_id = data.get("data", {}).get("bot_id")
|
|
36
|
+
print(f"Bot created: {bot_id}")
|
|
37
|
+
return bot_id
|
|
38
|
+
else:
|
|
39
|
+
error_msg = data.get("msg", "Unknown error")
|
|
40
|
+
print(f"Coze API Error: {error_msg}")
|
|
41
|
+
return None
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"Request Error: {e}")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def install_connector(self, connector_id: str, workspace_id: str) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Add a publishing channel (Connector) to the workspace.
|
|
49
|
+
e.g. connector_id for Douyin, Feishu, etc.
|
|
50
|
+
"""
|
|
51
|
+
url = f"{self.API_BASE}/connectors/{connector_id}/install"
|
|
52
|
+
payload = {
|
|
53
|
+
"workspace_id": workspace_id
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
resp = requests.post(url, headers=self.headers, json=payload)
|
|
58
|
+
data = resp.json()
|
|
59
|
+
if data.get("code") == 0:
|
|
60
|
+
print(f"Connector {connector_id} installed to workspace {workspace_id}")
|
|
61
|
+
return True
|
|
62
|
+
else:
|
|
63
|
+
print(f"Install Connector Error: {data.get('msg')}")
|
|
64
|
+
return False
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"Request Error: {e}")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def publish_bot(self, bot_id: str, connector_ids: List[str] = None) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Publish the bot to specified channels.
|
|
72
|
+
Default connector_ids: ['1024'] (API/SDK)
|
|
73
|
+
"""
|
|
74
|
+
if connector_ids is None:
|
|
75
|
+
connector_ids = ["1024"]
|
|
76
|
+
|
|
77
|
+
url = f"{self.API_BASE}/bot/publish"
|
|
78
|
+
payload = {
|
|
79
|
+
"bot_id": bot_id,
|
|
80
|
+
"connector_ids": connector_ids
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
resp = requests.post(url, headers=self.headers, json=payload)
|
|
85
|
+
data = resp.json()
|
|
86
|
+
if data.get("code") == 0:
|
|
87
|
+
print(f"Bot {bot_id} published to {connector_ids}")
|
|
88
|
+
return True
|
|
89
|
+
else:
|
|
90
|
+
print(f"Publish Bot Error: {data.get('msg')}")
|
|
91
|
+
return False
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"Publish Error: {e}")
|
|
94
|
+
return False
|
|
95
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
|
|
6
|
+
class QianwenApiClient:
|
|
7
|
+
# This is a placeholder for the specific Bailian/Model Studio Agent API
|
|
8
|
+
# As of now, Bailian Agent API might be complex.
|
|
9
|
+
# We'll mock the structure based on common patterns or use a documented endpoint if available.
|
|
10
|
+
# Usually involves: Create Application -> Publish.
|
|
11
|
+
|
|
12
|
+
API_BASE = "https://dashscope.aliyuncs.com/api/v1/apps" # Example endpoint
|
|
13
|
+
|
|
14
|
+
def __init__(self, api_key: str):
|
|
15
|
+
self.headers = {
|
|
16
|
+
"Authorization": f"Bearer {api_key}",
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
"X-DashScope-SSE": "disable" # Disable streaming for simple calls
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def create_agent(self, name: str, description: str, prompt: str) -> Optional[str]:
|
|
22
|
+
"""
|
|
23
|
+
Create an Agent/App on Bailian.
|
|
24
|
+
Returns app_id.
|
|
25
|
+
"""
|
|
26
|
+
# Note: Real endpoint might differ. Using a representative structure.
|
|
27
|
+
url = f"{self.API_BASE}"
|
|
28
|
+
payload = {
|
|
29
|
+
"name": name,
|
|
30
|
+
"description": description,
|
|
31
|
+
"prompt": prompt,
|
|
32
|
+
"model": "qwen-max" # Default model
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# POST to create
|
|
37
|
+
# If this endpoint is not public for agents, this will fail gracefully
|
|
38
|
+
# For now, we assume a standard pattern
|
|
39
|
+
resp = requests.post(url, headers=self.headers, json=payload)
|
|
40
|
+
data = resp.json()
|
|
41
|
+
|
|
42
|
+
# Check for success (structure varies)
|
|
43
|
+
if data.get("code") == 200 or "id" in data:
|
|
44
|
+
return data.get("id") or data.get("output", {}).get("app_id")
|
|
45
|
+
else:
|
|
46
|
+
print(f"Qianwen API Error: {data}")
|
|
47
|
+
return None
|
|
48
|
+
except Exception as e:
|
|
49
|
+
print(f"Request Error: {e}")
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def publish_agent(self, app_id: str) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Publish the agent.
|
|
55
|
+
"""
|
|
56
|
+
# Usually requires a separate publish call or versioning.
|
|
57
|
+
print(f"Publishing {app_id}...")
|
|
58
|
+
return True # Placeholder
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List, Optional, Dict, Any
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SkillRef:
|
|
10
|
+
name: str
|
|
11
|
+
content: str = ""
|
|
12
|
+
path: str = ""
|
|
13
|
+
loaded: bool = False
|
|
14
|
+
|
|
15
|
+
def load_content(self, root_dir: str):
|
|
16
|
+
"""Load the actual skill content from file system."""
|
|
17
|
+
# Try to find the skill file
|
|
18
|
+
# Structure: {root_dir}/{skill_name}/SKILL.md
|
|
19
|
+
potential_path = os.path.join(root_dir, self.name, "SKILL.md")
|
|
20
|
+
if os.path.exists(potential_path):
|
|
21
|
+
with open(potential_path, "r", encoding="utf-8") as f:
|
|
22
|
+
self.content = f.read()
|
|
23
|
+
self.path = potential_path
|
|
24
|
+
self.loaded = True
|
|
25
|
+
return True
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Recipe:
|
|
30
|
+
"""Recipe maps user intent to specific skills."""
|
|
31
|
+
name: str
|
|
32
|
+
trigger_keywords: List[str]
|
|
33
|
+
skills: List[str] = field(default_factory=list) # Skill names
|
|
34
|
+
notes: str = ""
|
|
35
|
+
routing: Optional[str] = None
|
|
36
|
+
_skill_refs: List[SkillRef] = field(default_factory=list, init=False)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def skill_refs(self) -> List[SkillRef]:
|
|
40
|
+
return self._skill_refs
|
|
41
|
+
|
|
42
|
+
def load_skills(self, skills_dir: str):
|
|
43
|
+
"""Load all skills defined in this recipe."""
|
|
44
|
+
self._skill_refs = []
|
|
45
|
+
for skill_name in self.skills:
|
|
46
|
+
ref = SkillRef(name=skill_name)
|
|
47
|
+
if ref.load_content(skills_dir):
|
|
48
|
+
self._skill_refs.append(ref)
|
|
49
|
+
else:
|
|
50
|
+
print(f"[Warning] Skill not found: {skill_name} in {skills_dir}")
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, data: Dict[str, Any], source_file: str = ""):
|
|
54
|
+
return cls(
|
|
55
|
+
name=data.get("name", Path(source_file).stem),
|
|
56
|
+
trigger_keywords=data.get("trigger_keywords", []),
|
|
57
|
+
skills=data.get("skills", []),
|
|
58
|
+
notes=data.get("notes", ""),
|
|
59
|
+
routing=data.get("routing")
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_json(cls, path: str):
|
|
64
|
+
with open(path, 'r', encoding="utf-8") as f:
|
|
65
|
+
data = json.load(f)
|
|
66
|
+
return cls.from_dict(data, path)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-assembler
|
|
3
|
+
Version: 0.2.0.dev1
|
|
4
|
+
Summary: Deterministic Context Assembly for AI Agents.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: streamlit
|
|
10
|
+
Requires-Dist: requests
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# Agent Assembler 🧩
|
|
15
|
+
|
|
16
|
+
> **Deterministic Context Assembly for AI Agents.**
|
|
17
|
+
> The Engine for the Multi-Agent Distribution Network.
|
|
18
|
+
|
|
19
|
+
⚠️ **Status**: Currently in active development (Phase 1: SDK Decoupling). Please clone from GitHub for now. `pip install` package coming soon in Phase 2.
|
|
20
|
+
|
|
21
|
+
## 1. Vision
|
|
22
|
+
**From JIT Engine to Agent Factory & Distribution Network.**
|
|
23
|
+
Agent Assembler is no longer just a script; it is the **core engine** that powers multi-agent systems across platforms (Qianwen, Coze, WeChat, etc.).
|
|
24
|
+
|
|
25
|
+
## 2. Core Architecture
|
|
26
|
+
- **Recipe-First**: Intent matching pre-defined JSON recipes.
|
|
27
|
+
- **Atomic Skills**: <4KB focused skill modules.
|
|
28
|
+
- **JIT Assembly**: Assemble only what is needed, when it is needed.
|
|
29
|
+
- **Multi-Platform Adapters**: Deploy to Qianwen, Coze, Baidu, and more with one click.
|
|
30
|
+
|
|
31
|
+
## 3. Roadmap
|
|
32
|
+
| Phase | Goal | Status |
|
|
33
|
+
|-------|------|--------|
|
|
34
|
+
| **P0** | Core Stabilization & Validation | ✅ Done |
|
|
35
|
+
| **P1** | **SDK Decoupling & Standardization** | 🚧 Active |
|
|
36
|
+
| **P2** | Multi-Platform Adapters (Coze/Qianwen) | ⬜ Planned |
|
|
37
|
+
| **P3** | SaaS Dashboard & No-Code Builder | ⬜ Planned |
|
|
38
|
+
|
|
39
|
+
## 4. Installation
|
|
40
|
+
```bash
|
|
41
|
+
pip install agent-assembler
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 5. Quick Start
|
|
45
|
+
```python
|
|
46
|
+
from agent_assembler import Assembler
|
|
47
|
+
|
|
48
|
+
assembler = Assembler(recipes_dir="./recipes", skills_dir="./skills")
|
|
49
|
+
result = assembler.assemble("Analyze this excel file")
|
|
50
|
+
print(result['system_prompt'])
|
|
51
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
agent_assembler/__init__.py,sha256=o2J8n-bbNtiwI2OBBfFFFtRgwaCb5t5VZo2oopJ9t_8,124
|
|
2
|
+
agent_assembler/assembler.py,sha256=SsCVYJzDC2EzoAm5qpdZXezJYon3mWsj39sS6Lotw1A,3073
|
|
3
|
+
agent_assembler/recipe.py,sha256=f1OSFN9AyfCnMSbZbAcMx0Yk1oyN92jF6atjzVVnh20,2130
|
|
4
|
+
agent_assembler/adapters/__init__.py,sha256=mXC0Tte8yCjm7kefiCw-sRfz-hiDIDKTKSreAEEyPb0,156
|
|
5
|
+
agent_assembler/adapters/base.py,sha256=VMXE4PvB25RysuaHU80aSs5TQ09DDHYI_jGXPvwFCJw,649
|
|
6
|
+
agent_assembler/adapters/coze.py,sha256=zlAVQrxrQiUCbvyVeNm_cDkr4JgEJqKHdnfYcfghApA,3378
|
|
7
|
+
agent_assembler/adapters/qianwen.py,sha256=B14UNGiK7Nsat68Pjtt509pjzbYIZd2S3JhFOdCrSP4,3862
|
|
8
|
+
agent_assembler/deploy/__init__.py,sha256=vCl4DSaIIeWFb76dhlhJaf6_KzKPY_AMwvstil6lhXQ,79
|
|
9
|
+
agent_assembler/deploy/coze_api.py,sha256=JcyDhF5T46DDrmOpCdOp01LVQ95LS1AwyfR5MyR7LIg,3096
|
|
10
|
+
agent_assembler/deploy/qianwen_api.py,sha256=3I1NS44bsQa9MOAIsLz8HhR1WAcZ39KwkOqYnEL1Sl4,2103
|
|
11
|
+
agent_assembler-0.2.0.dev1.dist-info/licenses/LICENSE,sha256=wWbdNra8DIz9HRQ77dt2fCUWx51WjfpYU-feI4YJ9g0,1070
|
|
12
|
+
agent_assembler-0.2.0.dev1.dist-info/METADATA,sha256=zHiJlLAgOmtI5zAYbFvi4s_UuS6y1Kf_jbRA6rNnB7k,1720
|
|
13
|
+
agent_assembler-0.2.0.dev1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
agent_assembler-0.2.0.dev1.dist-info/top_level.txt,sha256=l8ktHSG__P_oBMzzxqCFUNRZ8Jcf-t44zuX-GgXmfZM,16
|
|
15
|
+
agent_assembler-0.2.0.dev1.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Hermes Agent
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
agent_assembler
|