Skip to content

Commit bb93106

Browse files
authored
Merge pull request #2731 from ModelEngine-Group/xyc/dev_skills
✨ Support skill features, including creation, preview, set in agent version
2 parents 7123349 + 3988f7e commit bb93106

File tree

80 files changed

+23456
-2370
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+23456
-2370
lines changed

backend/agents/create_agent_info.py

Lines changed: 156 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import threading
22
import logging
3+
from typing import List
34
from urllib.parse import urljoin
45
from datetime import datetime
56

67
from jinja2 import Template, StrictUndefined
7-
from smolagents.utils import BASE_BUILTIN_MODULES
88
from nexent.core.utils.observer import MessageObserver
99
from nexent.core.agents.agent_model import AgentRunInfo, ModelConfig, AgentConfig, ToolConfig
1010
from nexent.memory.memory_service import search_memory_in_levels
@@ -27,11 +27,119 @@
2727
from utils.prompt_template_utils import get_agent_prompt_template
2828
from utils.config_utils import tenant_config_manager, get_model_name_from_config
2929
from consts.const import LOCAL_MCP_SERVER, MODEL_CONFIG_MAPPING, LANGUAGE, DATA_PROCESS_SERVICE
30+
import re
3031

3132
logger = logging.getLogger("create_agent_info")
3233
logger.setLevel(logging.DEBUG)
3334

3435

36+
def _get_skills_for_template(
37+
agent_id: int,
38+
tenant_id: str,
39+
version_no: int = 0
40+
) -> List[dict]:
41+
"""Get skills list for prompt template injection.
42+
43+
Args:
44+
agent_id: Agent ID
45+
tenant_id: Tenant ID
46+
version_no: Version number
47+
48+
Returns:
49+
List of skill dicts with name and description
50+
"""
51+
try:
52+
from services.skill_service import SkillService
53+
skill_service = SkillService()
54+
enabled_skills = skill_service.get_enabled_skills_for_agent(
55+
agent_id=agent_id,
56+
tenant_id=tenant_id,
57+
version_no=version_no
58+
)
59+
return [
60+
{"name": s.get("name", ""), "description": s.get("description", "")}
61+
for s in enabled_skills
62+
]
63+
except Exception as e:
64+
logger.warning(f"Failed to get skills for template: {e}")
65+
return []
66+
67+
68+
def _get_skill_script_tools(
69+
agent_id: int,
70+
tenant_id: str,
71+
version_no: int = 0
72+
) -> List[ToolConfig]:
73+
"""Get tool config for skill script execution and skill reading.
74+
75+
Args:
76+
agent_id: Agent ID for filtering available skills in error messages.
77+
tenant_id: Tenant ID for filtering available skills in error messages.
78+
version_no: Version number for filtering available skills.
79+
80+
Returns:
81+
List of ToolConfig for skill execution and reading tools
82+
"""
83+
from consts.const import CONTAINER_SKILLS_PATH
84+
85+
skill_context = {
86+
"agent_id": agent_id,
87+
"tenant_id": tenant_id,
88+
"version_no": version_no,
89+
}
90+
91+
try:
92+
return [
93+
ToolConfig(
94+
class_name="RunSkillScriptTool",
95+
name="run_skill_script",
96+
description="Execute a skill script with given parameters. Use this to run Python or shell scripts that are part of a skill.",
97+
inputs='{"skill_name": "str", "script_path": "str", "params": "dict"}',
98+
output_type="string",
99+
params={"local_skills_dir": CONTAINER_SKILLS_PATH},
100+
source="builtin",
101+
usage="builtin",
102+
metadata=skill_context,
103+
),
104+
ToolConfig(
105+
class_name="ReadSkillMdTool",
106+
name="read_skill_md",
107+
description="Read skill execution guide and optional additional files. Always reads SKILL.md first, then optionally reads additional files.",
108+
inputs='{"skill_name": "str", "additional_files": "list[str]"}',
109+
output_type="string",
110+
params={"local_skills_dir": CONTAINER_SKILLS_PATH},
111+
source="builtin",
112+
usage="builtin",
113+
metadata=skill_context,
114+
),
115+
ToolConfig(
116+
class_name="ReadSkillConfigTool",
117+
name="read_skill_config",
118+
description="Read the config.yaml file from a skill directory. Returns JSON containing configuration variables needed for skill workflows.",
119+
inputs='{"skill_name": "str"}',
120+
output_type="string",
121+
params={"local_skills_dir": CONTAINER_SKILLS_PATH},
122+
source="builtin",
123+
usage="builtin",
124+
metadata=skill_context,
125+
),
126+
ToolConfig(
127+
class_name="WriteSkillFileTool",
128+
name="write_skill_file",
129+
description="Write content to a file within a skill directory. Creates parent directories if they do not exist.",
130+
inputs='{"skill_name": "str", "file_path": "str", "content": "str"}',
131+
output_type="string",
132+
params={"local_skills_dir": CONTAINER_SKILLS_PATH},
133+
source="builtin",
134+
usage="builtin",
135+
metadata=skill_context,
136+
)
137+
]
138+
except Exception as e:
139+
logger.warning(f"Failed to load skill script tool: {e}")
140+
return []
141+
142+
35143
async def create_model_config_list(tenant_id):
36144
records = get_model_records({"model_type": "llm"}, tenant_id)
37145
model_list = []
@@ -169,22 +277,26 @@ async def create_agent_config(
169277
logger.error(f"Failed to build knowledge base summary: {e}")
170278

171279
# Assemble system_prompt
172-
if duty_prompt or constraint_prompt or few_shots_prompt:
173-
system_prompt = Template(prompt_template["system_prompt"], undefined=StrictUndefined).render({
174-
"duty": duty_prompt,
175-
"constraint": constraint_prompt,
176-
"few_shots": few_shots_prompt,
177-
"tools": {tool.name: tool for tool in tool_list},
178-
"managed_agents": {agent.name: agent for agent in managed_agents},
179-
"authorized_imports": str(BASE_BUILTIN_MODULES),
180-
"APP_NAME": app_name,
181-
"APP_DESCRIPTION": app_description,
182-
"memory_list": memory_list,
183-
"knowledge_base_summary": knowledge_base_summary,
184-
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
185-
})
186-
else:
187-
system_prompt = agent_info.get("prompt", "")
280+
# Get skills list for prompt template
281+
skills = _get_skills_for_template(agent_id, tenant_id, version_no)
282+
283+
render_kwargs = {
284+
"duty": duty_prompt,
285+
"constraint": constraint_prompt,
286+
"few_shots": few_shots_prompt,
287+
"tools": {tool.name: tool for tool in tool_list},
288+
"skills": skills,
289+
"managed_agents": {agent.name: agent for agent in managed_agents},
290+
"APP_NAME": app_name,
291+
"APP_DESCRIPTION": app_description,
292+
"memory_list": memory_list,
293+
"knowledge_base_summary": knowledge_base_summary,
294+
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
295+
"user_id": user_id,
296+
}
297+
system_prompt = Template(prompt_template["system_prompt"], undefined=StrictUndefined).render(render_kwargs)
298+
299+
_print_prompt_with_token_count(system_prompt, agent_id, "BEFORE_INJECTION")
188300

189301
if agent_info.get("model_id") is not None:
190302
model_info = get_model_by_model_id(agent_info.get("model_id"))
@@ -197,9 +309,10 @@ async def create_agent_config(
197309
prompt_templates=await prepare_prompt_templates(
198310
is_manager=len(managed_agents) > 0,
199311
system_prompt=system_prompt,
200-
language=language
312+
language=language,
313+
agent_id=agent_id
201314
),
202-
tools=tool_list,
315+
tools=tool_list + _get_skill_script_tools(agent_id, tenant_id, version_no),
203316
max_steps=agent_info.get("max_steps", 10),
204317
model_name=model_name,
205318
provide_run_summary=agent_info.get("provide_run_summary", False),
@@ -296,23 +409,46 @@ async def discover_langchain_tools():
296409
return langchain_tools
297410

298411

299-
async def prepare_prompt_templates(is_manager: bool, system_prompt: str, language: str = 'zh'):
412+
async def prepare_prompt_templates(
413+
is_manager: bool,
414+
system_prompt: str,
415+
language: str = 'zh',
416+
agent_id: int = None,
417+
):
300418
"""
301419
Prepare prompt templates, support multiple languages
302420
303421
Args:
304422
is_manager: Whether it is a manager mode
305423
system_prompt: System prompt content
306424
language: Language code ('zh' or 'en')
425+
agent_id: Agent ID for fetching skill instances
307426
308427
Returns:
309428
dict: Prompt template configuration
310429
"""
311430
prompt_templates = get_agent_prompt_template(is_manager, language)
312431
prompt_templates["system_prompt"] = system_prompt
432+
433+
# Print final prompt with all injections
434+
_print_prompt_with_token_count(prompt_templates["system_prompt"], agent_id, "FINAL_PROMPT")
435+
313436
return prompt_templates
314437

315438

439+
def _print_prompt_with_token_count(prompt: str, agent_id: int = None, stage: str = "PROMPT"):
440+
"""Print prompt content and estimate token count using tiktoken."""
441+
try:
442+
import tiktoken
443+
encoding = tiktoken.get_encoding("cl100k_base")
444+
token_count = len(encoding.encode(prompt))
445+
logger.info(f"[Skill Debug][{stage}] Agent {agent_id} token count: {token_count}")
446+
logger.info(f"[Skill Debug][{stage}] Agent {agent_id} prompt:\n{prompt}")
447+
except Exception as e:
448+
logger.warning(f"[Skill Debug][{stage}] Failed to count tokens: {e}")
449+
logger.info(f"[Skill Debug][{stage}] Agent {agent_id} prompt:\n{prompt}")
450+
451+
316452
async def join_minio_file_description_to_query(minio_files, query):
317453
final_query = query
318454
if minio_files and isinstance(minio_files, list):

backend/apps/agent_app.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
run_agent_stream,
2121
stop_agent_tasks,
2222
get_agent_call_relationship_impl,
23-
clear_agent_new_mark_impl
23+
clear_agent_new_mark_impl,
24+
get_agent_by_name_impl,
2425
)
2526
from services.agent_version_service import (
2627
publish_version_impl,
@@ -100,6 +101,27 @@ async def search_agent_info_api(
100101
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Agent search info error.")
101102

102103

104+
@agent_config_router.get("/by-name/{agent_name}")
105+
async def get_agent_by_name_api(
106+
agent_name: str,
107+
tenant_id: Optional[str] = Query(
108+
None, description="Tenant ID for filtering (uses auth if not provided)"),
109+
authorization: Optional[str] = Header(None)
110+
):
111+
"""
112+
Look up an agent by name and return its agent_id and highest published version_no.
113+
"""
114+
try:
115+
_, auth_tenant_id = get_current_user_id(authorization)
116+
effective_tenant_id = tenant_id or auth_tenant_id
117+
result = get_agent_by_name_impl(agent_name, effective_tenant_id)
118+
return JSONResponse(status_code=HTTPStatus.OK, content=result)
119+
except Exception as e:
120+
logger.error(f"Agent by name lookup error: {str(e)}")
121+
raise HTTPException(
122+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Agent not found.")
123+
124+
103125
@agent_config_router.get("/get_creating_sub_agent_id")
104126
async def get_creating_sub_agent_info_api(authorization: Optional[str] = Header(None)):
105127
"""

backend/apps/config_app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from apps.model_managment_app import router as model_manager_router
1515
from apps.prompt_app import router as prompt_router
1616
from apps.remote_mcp_app import router as remote_mcp_router
17+
from apps.skill_app import router as skill_router
1718
from apps.tenant_config_app import router as tenant_config_router
1819
from apps.tool_config_app import router as tool_config_router
1920
from apps.user_management_app import router as user_management_router
@@ -52,6 +53,7 @@
5253

5354
app.include_router(summary_router)
5455
app.include_router(prompt_router)
56+
app.include_router(skill_router)
5557
app.include_router(tenant_config_router)
5658
app.include_router(remote_mcp_router)
5759
app.include_router(tenant_router)

0 commit comments

Comments
 (0)