agent-assembler 0.2.0__tar.gz

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.
@@ -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,6 @@
1
+
2
+ include README.md
3
+ include LICENSE
4
+ include pyproject.toml
5
+ recursive-include tests *.py
6
+ recursive-include agent_assembler *.py *.md
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-assembler
3
+ Version: 0.2.0
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,38 @@
1
+ # Agent Assembler 🧩
2
+
3
+ > **Deterministic Context Assembly for AI Agents.**
4
+ > The Engine for the Multi-Agent Distribution Network.
5
+
6
+ āš ļø **Status**: Currently in active development (Phase 1: SDK Decoupling). Please clone from GitHub for now. `pip install` package coming soon in Phase 2.
7
+
8
+ ## 1. Vision
9
+ **From JIT Engine to Agent Factory & Distribution Network.**
10
+ Agent Assembler is no longer just a script; it is the **core engine** that powers multi-agent systems across platforms (Qianwen, Coze, WeChat, etc.).
11
+
12
+ ## 2. Core Architecture
13
+ - **Recipe-First**: Intent matching pre-defined JSON recipes.
14
+ - **Atomic Skills**: <4KB focused skill modules.
15
+ - **JIT Assembly**: Assemble only what is needed, when it is needed.
16
+ - **Multi-Platform Adapters**: Deploy to Qianwen, Coze, Baidu, and more with one click.
17
+
18
+ ## 3. Roadmap
19
+ | Phase | Goal | Status |
20
+ |-------|------|--------|
21
+ | **P0** | Core Stabilization & Validation | āœ… Done |
22
+ | **P1** | **SDK Decoupling & Standardization** | 🚧 Active |
23
+ | **P2** | Multi-Platform Adapters (Coze/Qianwen) | ⬜ Planned |
24
+ | **P3** | SaaS Dashboard & No-Code Builder | ⬜ Planned |
25
+
26
+ ## 4. Installation
27
+ ```bash
28
+ pip install agent-assembler
29
+ ```
30
+
31
+ ## 5. Quick Start
32
+ ```python
33
+ from agent_assembler import Assembler
34
+
35
+ assembler = Assembler(recipes_dir="./recipes", skills_dir="./skills")
36
+ result = assembler.assemble("Analyze this excel file")
37
+ print(result['system_prompt'])
38
+ ```
@@ -0,0 +1,19 @@
1
+
2
+ [build-system]
3
+ requires = ["setuptools>=61.0"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ name = "agent-assembler"
8
+ version = "0.2.0"
9
+ description = "Deterministic Context Assembly for AI Agents."
10
+ readme = "README.md"
11
+ requires-python = ">=3.8"
12
+ license = {text = "MIT"}
13
+ dynamic = ["dependencies"]
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
17
+
18
+ [tool.setuptools.dynamic]
19
+ dependencies = {file = ["requirements.txt"]}
@@ -0,0 +1,4 @@
1
+
2
+ streamlit
3
+ requests
4
+ pandas
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+
2
+ from .assembler import Assembler
3
+ from .recipe import Recipe
4
+
5
+ __version__ = "0.2.0.dev1"
6
+
7
+ __all__ = ["Assembler", "Recipe"]
@@ -0,0 +1,5 @@
1
+ from .base import BaseAdapter
2
+ from .coze import CozeAdapter
3
+ from .qianwen import QianwenAdapter
4
+
5
+ __all__ = ["BaseAdapter", "CozeAdapter", "QianwenAdapter"]
@@ -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,3 @@
1
+
2
+ from .coze_api import CozeApiClient
3
+ from .qianwen_api import QianwenApiClient
@@ -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
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,23 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ requirements.txt
6
+ src/agent_assembler/__init__.py
7
+ src/agent_assembler/assembler.py
8
+ src/agent_assembler/recipe.py
9
+ src/agent_assembler.egg-info/PKG-INFO
10
+ src/agent_assembler.egg-info/SOURCES.txt
11
+ src/agent_assembler.egg-info/dependency_links.txt
12
+ src/agent_assembler.egg-info/requires.txt
13
+ src/agent_assembler.egg-info/top_level.txt
14
+ src/agent_assembler/adapters/__init__.py
15
+ src/agent_assembler/adapters/base.py
16
+ src/agent_assembler/adapters/coze.py
17
+ src/agent_assembler/adapters/qianwen.py
18
+ src/agent_assembler/deploy/__init__.py
19
+ src/agent_assembler/deploy/coze_api.py
20
+ src/agent_assembler/deploy/qianwen_api.py
21
+ tests/__init__.py
22
+ tests/test_adapters.py
23
+ tests/test_assembler.py
@@ -0,0 +1,3 @@
1
+ streamlit
2
+ requests
3
+ pandas
@@ -0,0 +1 @@
1
+ agent_assembler
File without changes
@@ -0,0 +1,160 @@
1
+ """Tests for Agent Assembler Adapters."""
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ import json
6
+
7
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
8
+
9
+ from agent_assembler.recipe import Recipe
10
+ from agent_assembler.adapters import CozeAdapter, QianwenAdapter
11
+
12
+
13
+ def test_coze_export_basic():
14
+ """Test: CozeAdapter exports valid DSL structure."""
15
+ recipe = Recipe(
16
+ name="ę•°ę®åˆ†ęžåŠ©ę‰‹",
17
+ trigger_keywords=["ę•°ę®åˆ†ęž", "excel"],
18
+ skills=[],
19
+ notes="专门处理 Excel ę•°ę®åˆ†ęžä»»åŠ”"
20
+ )
21
+
22
+ adapter = CozeAdapter()
23
+ result = adapter.export(recipe)
24
+
25
+ assert "bot_info" in result
26
+ assert result["bot_info"]["name"] == "ę•°ę®åˆ†ęžåŠ©ę‰‹"
27
+ assert result["bot_info"]["prompt_info"]["prompt"] != ""
28
+ assert result["model_info"]["model_name"] == "gpt-4o"
29
+ assert result["metadata"]["source"] == "agent-assembler"
30
+ print("āœ… test_coze_export_basic passed")
31
+
32
+
33
+ def test_coze_skills_injection():
34
+ """Test: CozeAdapter injects skill content into prompt."""
35
+ with tempfile.TemporaryDirectory() as tmpdir:
36
+ os.makedirs(os.path.join(tmpdir, "data_clean"))
37
+ with open(os.path.join(tmpdir, "data_clean", "SKILL.md"), "w") as f:
38
+ f.write("# ę•°ę®ęø…ę“—\nę­„éŖ¤: 1. 去重 2. ę ¼å¼åŒ– 3. 栔验")
39
+
40
+ recipe = Recipe(
41
+ name="ę•°ę®å¤„ē†",
42
+ trigger_keywords=["ę•°ę®"],
43
+ skills=["data_clean"]
44
+ )
45
+
46
+ adapter = CozeAdapter(skills_dir=tmpdir)
47
+ result = adapter.export(recipe)
48
+ prompt = result["bot_info"]["prompt_info"]["prompt"]
49
+
50
+ assert "ę•°ę®ęø…ę“—" in prompt
51
+ assert "去重" in prompt
52
+ print("āœ… test_coze_skills_injection passed")
53
+
54
+
55
+ def test_coze_validate():
56
+ """Test: CozeAdapter validates recipe constraints."""
57
+ adapter = CozeAdapter()
58
+
59
+ # Valid recipe
60
+ good = Recipe(name="ēŸ­å", trigger_keywords=["test"], skills=[])
61
+ assert len(adapter.validate(good)) == 0
62
+
63
+ # Name too long
64
+ long_name = Recipe(name="A" * 51, trigger_keywords=["test"], skills=[])
65
+ errors = adapter.validate(long_name)
66
+ assert len(errors) > 0
67
+ assert "50" in errors[0]
68
+
69
+ print("āœ… test_coze_validate passed")
70
+
71
+
72
+ def test_qianwen_export_basic():
73
+ """Test: QianwenAdapter exports valid DSL structure."""
74
+ recipe = Recipe(
75
+ name="å®¢ęœåŠ©ę‰‹",
76
+ trigger_keywords=["å®¢ęœ", "咨询"],
77
+ notes="å¤„ē†å®¢ęˆ·å’ØčÆ¢å’ŒęŠ•čÆ‰",
78
+ routing="operations-venue-agent"
79
+ )
80
+
81
+ adapter = QianwenAdapter()
82
+ result = adapter.export(recipe)
83
+
84
+ assert "name" in result
85
+ assert result["name"] == "å®¢ęœåŠ©ę‰‹"
86
+ assert "system_prompt" in result
87
+ assert result["model"] == "qwen-max"
88
+ assert result["metadata"]["routing"] == "operations-venue-agent"
89
+ print("āœ… test_qianwen_export_basic passed")
90
+
91
+
92
+ def test_qianwen_skills_injection():
93
+ """Test: QianwenAdapter injects skill content into prompt."""
94
+ with tempfile.TemporaryDirectory() as tmpdir:
95
+ os.makedirs(os.path.join(tmpdir, "reply_template"))
96
+ with open(os.path.join(tmpdir, "reply_template", "SKILL.md"), "w") as f:
97
+ f.write("# å›žå¤ęØ”ęæ\n规范: ē¤¼č²Œć€äø“äøšć€ē®€ę“")
98
+
99
+ recipe = Recipe(
100
+ name="ę™ŗčƒ½å®¢ęœ",
101
+ trigger_keywords=["回复"],
102
+ skills=["reply_template"]
103
+ )
104
+
105
+ adapter = QianwenAdapter(skills_dir=tmpdir)
106
+ result = adapter.export(recipe)
107
+ prompt = result["system_prompt"]
108
+
109
+ assert "å›žå¤ęØ”ęæ" in prompt
110
+ assert "礼貌" in prompt
111
+ print("āœ… test_qianwen_skills_injection passed")
112
+
113
+
114
+ def test_qianwen_validate():
115
+ """Test: QianwenAdapter validates recipe constraints."""
116
+ adapter = QianwenAdapter()
117
+
118
+ # Valid recipe
119
+ good = Recipe(name="ę­£åøøå", trigger_keywords=["test"], skills=[])
120
+ assert len(adapter.validate(good)) == 0
121
+
122
+ # Name too long (30 char limit for Qianwen)
123
+ long_name = Recipe(name="A" * 31, trigger_keywords=["test"], skills=[])
124
+ errors = adapter.validate(long_name)
125
+ assert len(errors) > 0
126
+ assert "30" in errors[0]
127
+
128
+ print("āœ… test_qianwen_validate passed")
129
+
130
+
131
+ def test_adapter_missing_skill_warning():
132
+ """Test: Adapter handles missing skill gracefully."""
133
+ recipe = Recipe(
134
+ name="测试",
135
+ trigger_keywords=["test"],
136
+ skills=["nonexistent_skill"]
137
+ )
138
+
139
+ coze = CozeAdapter(skills_dir="/tmp")
140
+ result = coze.export(recipe)
141
+ assert "nonexistent_skill" in result["bot_info"]["prompt_info"]["prompt"]
142
+ assert "not found" in result["bot_info"]["prompt_info"]["prompt"]
143
+
144
+ qianwen = QianwenAdapter(skills_dir="/tmp")
145
+ result = qianwen.export(recipe)
146
+ assert "nonexistent_skill" in result["system_prompt"]
147
+ assert "ęœŖę‰¾åˆ°" in result["system_prompt"]
148
+
149
+ print("āœ… test_adapter_missing_skill_warning passed")
150
+
151
+
152
+ if __name__ == "__main__":
153
+ test_coze_export_basic()
154
+ test_coze_skills_injection()
155
+ test_coze_validate()
156
+ test_qianwen_export_basic()
157
+ test_qianwen_skills_injection()
158
+ test_qianwen_validate()
159
+ test_adapter_missing_skill_warning()
160
+ print("\nāœ… All 7 adapter tests passed")
@@ -0,0 +1,148 @@
1
+ """Tests for Agent Assembler SDK."""
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ import json
6
+
7
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
8
+
9
+ from agent_assembler import Assembler, Recipe
10
+ from agent_assembler.recipe import SkillRef
11
+ from agent_assembler.adapters.base import BaseAdapter
12
+
13
+
14
+ def test_basic_assembly():
15
+ """Test: query matches recipe, skills loaded, prompt assembled."""
16
+ with tempfile.TemporaryDirectory() as tmpdir:
17
+ recipes_dir = os.path.join(tmpdir, "recipes")
18
+ skills_dir = os.path.join(tmpdir, "skills")
19
+ os.makedirs(recipes_dir)
20
+ os.makedirs(os.path.join(skills_dir, "skill_A"))
21
+
22
+ recipe = {
23
+ "name": "test_recipe",
24
+ "trigger_keywords": ["test", "demo"],
25
+ "skills": ["skill_A"]
26
+ }
27
+ with open(os.path.join(recipes_dir, "test.json"), "w") as f:
28
+ json.dump(recipe, f)
29
+
30
+ with open(os.path.join(skills_dir, "skill_A", "SKILL.md"), "w") as f:
31
+ f.write("# Skill A\nContent A")
32
+
33
+ assembler = Assembler(recipes_dir, skills_dir)
34
+ result = assembler.assemble("Please run a test demo")
35
+
36
+ assert result["status"] == "success", f"Status mismatch: {result['status']}"
37
+ assert result["recipe"] == "test_recipe"
38
+ assert "Content A" in result["system_prompt"]
39
+ print("āœ… test_basic_assembly passed")
40
+
41
+
42
+ def test_no_match_fallback():
43
+ """Test: query does not match any recipe → fallback."""
44
+ with tempfile.TemporaryDirectory() as tmpdir:
45
+ recipes_dir = os.path.join(tmpdir, "recipes")
46
+ skills_dir = os.path.join(tmpdir, "skills")
47
+ os.makedirs(recipes_dir)
48
+ os.makedirs(skills_dir)
49
+
50
+ recipe = {
51
+ "name": "only_recipe",
52
+ "trigger_keywords": ["specific"],
53
+ "skills": []
54
+ }
55
+ with open(os.path.join(recipes_dir, "only.json"), "w") as f:
56
+ json.dump(recipe, f)
57
+
58
+ assembler = Assembler(recipes_dir, skills_dir)
59
+ result = assembler.assemble("unrelated query")
60
+
61
+ assert result["status"] == "fallback"
62
+ assert "No matching recipe found" in result["message"]
63
+ print("āœ… test_no_match_fallback passed")
64
+
65
+
66
+ def test_missing_recipe_dir():
67
+ """Test: non-existent recipes directory raises FileNotFoundError."""
68
+ try:
69
+ Assembler("/nonexistent/path/12345", "/tmp")
70
+ assert False, "Should have raised FileNotFoundError"
71
+ except FileNotFoundError:
72
+ print("āœ… test_missing_recipe_dir passed")
73
+
74
+
75
+ def test_recipe_from_json():
76
+ """Test: Recipe deserialization from JSON."""
77
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
78
+ json.dump({
79
+ "name": "my_recipe",
80
+ "trigger_keywords": ["alpha", "beta"],
81
+ "skills": ["s1", "s2"],
82
+ "notes": "Some notes",
83
+ "routing": "engineering-agent"
84
+ }, f)
85
+ f.flush()
86
+
87
+ recipe = Recipe.from_json(f.name)
88
+ assert recipe.name == "my_recipe"
89
+ assert len(recipe.trigger_keywords) == 2
90
+ assert len(recipe.skills) == 2
91
+ assert recipe.notes == "Some notes"
92
+ assert recipe.routing == "engineering-agent"
93
+ print("āœ… test_recipe_from_json passed")
94
+
95
+
96
+ def test_skill_ref_load():
97
+ """Test: SkillRef loads content from filesystem."""
98
+ with tempfile.TemporaryDirectory() as tmpdir:
99
+ os.makedirs(os.path.join(tmpdir, "my_skill"))
100
+ with open(os.path.join(tmpdir, "my_skill", "SKILL.md"), "w") as f:
101
+ f.write("# My Skill\nBody content")
102
+
103
+ ref = SkillRef(name="my_skill")
104
+ assert ref.load_content(tmpdir) is True
105
+ assert ref.loaded is True
106
+ assert "Body content" in ref.content
107
+ print("āœ… test_skill_ref_load passed")
108
+
109
+
110
+ def test_skill_ref_missing():
111
+ """Test: SkillRef returns False when file does not exist."""
112
+ ref = SkillRef(name="nonexistent_skill")
113
+ assert ref.load_content("/tmp") is False
114
+ assert ref.loaded is False
115
+ print("āœ… test_skill_ref_missing passed")
116
+
117
+
118
+ def test_multiple_recipes_first_match():
119
+ """Test: first matching recipe is returned (deterministic)."""
120
+ with tempfile.TemporaryDirectory() as tmpdir:
121
+ recipes_dir = os.path.join(tmpdir, "recipes")
122
+ skills_dir = os.path.join(tmpdir, "skills")
123
+ os.makedirs(recipes_dir)
124
+ os.makedirs(skills_dir)
125
+
126
+ # Write two recipes that both match "data"
127
+ for name, kw in [("data_cleaner", ["data", "clean"]), ("data_viz", ["data", "chart"])]:
128
+ with open(os.path.join(recipes_dir, f"{name}.json"), "w") as f:
129
+ json.dump({"name": name, "trigger_keywords": kw, "skills": []}, f)
130
+
131
+ assembler = Assembler(recipes_dir, skills_dir)
132
+ result = assembler.assemble("show me data")
133
+
134
+ assert result["status"] == "success"
135
+ # Should match the first one found (alphabetical walk order)
136
+ assert result["recipe"] in ("data_cleaner", "data_viz")
137
+ print("āœ… test_multiple_recipes_first_match passed")
138
+
139
+
140
+ if __name__ == "__main__":
141
+ test_basic_assembly()
142
+ test_no_match_fallback()
143
+ test_missing_recipe_dir()
144
+ test_recipe_from_json()
145
+ test_skill_ref_load()
146
+ test_skill_ref_missing()
147
+ test_multiple_recipes_first_match()
148
+ print("\nāœ… All 7 tests passed")