Skip to content

Commit de58fa5

Browse files
alcholiclgalcholiclg
andauthored
Feat/enhance dsv2 (#886)
Co-authored-by: alcholiclg <ligongshengzju@foxmail.com>
1 parent 4420df4 commit de58fa5

32 files changed

+3621
-522
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ apps/agentfabric/config/local_user/*
153153
ast_index_file.py
154154

155155

156-
#neo4j
156+
# neo4j
157157
.neo4j.lock
158158
neo4j.lock
159159
/temp/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ For more details, please refer to [**MS-Agent Skills**](ms_agent/skill/README.md
308308
---
309309
310310
311-
### Agentic Insight
311+
### Agentic Insight (Deep Research)
312312
313313
#### - Lightweight, Efficient, and Extensible Multi-modal Deep Research Framework
314314

README_ZH.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,8 +311,7 @@ asyncio.run(main())
311311

312312
---
313313

314-
315-
### Agentic Insight
314+
### Agentic Insight (Deep Research)
316315

317316
#### - 轻量级、高效且可扩展的多模态深度研究框架
318317

ms_agent/config/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from copy import deepcopy
66
from typing import Any, Dict, Union
77

8+
from ms_agent.prompting import apply_prompt_files
89
from ms_agent.utils import get_logger
910
from omegaconf import DictConfig, ListConfig, OmegaConf
1011
from omegaconf.basecontainer import BaseContainer
@@ -95,6 +96,14 @@ def from_task(cls,
9596
config.local_dir = config_dir_or_id
9697
config.name = name
9798
config = cls.fill_missing_fields(config)
99+
# Prompt files: resolve config.prompt.system from prompts/ directory
100+
# if user didn't specify inline prompt.system.
101+
try:
102+
if isinstance(config, DictConfig):
103+
config = apply_prompt_files(config)
104+
except Exception:
105+
# Never block config loading due to prompt resolving.
106+
pass
98107
return config
99108

100109
@staticmethod

ms_agent/prompting/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) ModelScope Contributors. All rights reserved.
2+
from .file_resolver import apply_prompt_files, resolve_prompt_file
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Copyright (c) ModelScope Contributors. All rights reserved.
2+
import os
3+
from dataclasses import dataclass
4+
from typing import List, Optional, Tuple
5+
6+
from omegaconf import DictConfig
7+
8+
9+
@dataclass(frozen=True)
10+
class PromptFileSpec:
11+
agent: str
12+
lang: str
13+
family: str
14+
root_dir: str
15+
16+
def candidate_paths(self) -> List[str]:
17+
"""Return candidate prompt file paths in priority order."""
18+
# File convention: prompts/{agent}/{lang}/{family}.md
19+
# Fallback: family -> base
20+
agent = self.agent.strip()
21+
lang = self.lang.strip()
22+
family = self.family.strip()
23+
root = self.root_dir
24+
25+
paths = []
26+
if family:
27+
paths.extend([
28+
os.path.join(root, agent, lang, f'{family}.txt'),
29+
os.path.join(root, agent, lang, f'{family}.md'),
30+
])
31+
# base fallback
32+
paths.extend([
33+
os.path.join(root, agent, lang, 'base.txt'),
34+
os.path.join(root, agent, lang, 'base.md')
35+
])
36+
return paths
37+
38+
39+
def _norm_lang(lang: Optional[str]) -> str:
40+
if not lang:
41+
return 'zh'
42+
lang = str(lang).strip().lower()
43+
if lang in {'zh', 'zh-cn', 'zh_cn', 'cn'}:
44+
return 'zh'
45+
if lang in {'en', 'en-us', 'en_us', 'us'}:
46+
return 'en'
47+
if lang == 'auto':
48+
# We cannot reliably detect user language at config-load time,
49+
# so treat "auto" as default language (with env override handled elsewhere).
50+
return 'zh'
51+
return lang
52+
53+
54+
def _infer_family_from_model(model: Optional[str]) -> str:
55+
"""Infer a reasonable prompt family name from model string.
56+
57+
Notes:
58+
- This is a best-effort heuristic to keep user onboarding simple.
59+
- Users can always override via `prompt.family`.
60+
"""
61+
if not model:
62+
return 'base'
63+
m = str(model).strip().lower()
64+
65+
# Qwen series
66+
if 'qwen' in m:
67+
# Common variants: qwen3-*, qwen-3, qwen2.5-*, Qwen/Qwen3-...
68+
if 'qwen3' in m or 'qwen-3' in m or 'qwen/qwen3' in m:
69+
return 'qwen-3'
70+
if 'qwen2' in m or 'qwen-2' in m:
71+
return 'qwen-2'
72+
if 'qwen1' in m or 'qwen-1' in m:
73+
return 'qwen-1'
74+
return 'qwen'
75+
76+
# Claude series
77+
if 'claude' in m:
78+
return 'claude'
79+
80+
# GPT-like series (OpenAI / compatible)
81+
if 'gpt' in m or m.startswith('o1') or m.startswith('o3'):
82+
return 'gpt'
83+
84+
return 'base'
85+
86+
87+
def _get_prompt_root_dir(config: DictConfig) -> Optional[str]:
88+
"""Resolve prompts root directory.
89+
90+
Priority:
91+
- config.prompt.root (absolute or relative to config.local_dir)
92+
- <config.local_dir>/prompts
93+
"""
94+
local_dir = getattr(config, 'local_dir', None)
95+
prompt_cfg = getattr(config, 'prompt', None)
96+
root = None
97+
if isinstance(prompt_cfg, DictConfig):
98+
root = getattr(prompt_cfg, 'root', None)
99+
100+
if root:
101+
root = str(root).strip()
102+
if not root:
103+
root = None
104+
elif not os.path.isabs(root) and local_dir:
105+
root = os.path.join(str(local_dir), root)
106+
107+
if not root and local_dir:
108+
root = os.path.join(str(local_dir), 'prompts')
109+
110+
return root
111+
112+
113+
def _get_prompt_agent(config: DictConfig) -> Optional[str]:
114+
"""Resolve agent name used in prompts/{agent}/... path."""
115+
prompt_cfg = getattr(config, 'prompt', None)
116+
if isinstance(prompt_cfg, DictConfig):
117+
agent = getattr(prompt_cfg, 'agent', None)
118+
if agent:
119+
agent = str(agent).strip()
120+
if agent:
121+
return agent
122+
123+
# Prefer `code_file` for project agents (deep_research v2 uses this)
124+
code_file = getattr(config, 'code_file', None)
125+
if code_file:
126+
code_file = str(code_file).strip()
127+
if code_file:
128+
return code_file
129+
130+
# Fallback: try `tag` (may be too specific; we only use it if user opts in via prompt.agent)
131+
return None
132+
133+
134+
def _get_prompt_lang_and_family(config: DictConfig) -> Tuple[str, str]:
135+
prompt_cfg = getattr(config, 'prompt', None)
136+
137+
# lang
138+
env_lang = os.environ.get('MS_AGENT_PROMPT_LANG') or os.environ.get(
139+
'MS_AGENT_LANG')
140+
cfg_lang = getattr(prompt_cfg, 'lang', None) if isinstance(
141+
prompt_cfg, DictConfig) else None
142+
lang = _norm_lang(cfg_lang or env_lang or 'zh')
143+
144+
# family
145+
env_family = os.environ.get('MS_AGENT_PROMPT_FAMILY')
146+
cfg_family = getattr(prompt_cfg, 'family', None) if isinstance(
147+
prompt_cfg, DictConfig) else None
148+
149+
family = (cfg_family or env_family or 'auto')
150+
family = str(family).strip()
151+
if not family:
152+
family = 'auto'
153+
if family.lower() == 'auto':
154+
model = None
155+
if hasattr(config, 'llm') and getattr(config, 'llm') is not None:
156+
try:
157+
model = getattr(config.llm, 'model', None)
158+
except Exception:
159+
model = None
160+
family = _infer_family_from_model(model)
161+
return lang, family
162+
163+
164+
def resolve_prompt_file(config: DictConfig) -> Optional[str]:
165+
"""Resolve system prompt text from prompt files.
166+
167+
Returns:
168+
Prompt text if a file is found, else None.
169+
170+
Compatibility rules:
171+
- If `prompt.system` exists and is non-empty, this resolver is NOT used.
172+
- Resolver is only eligible when we can infer a prompt agent name (or user provided prompt.agent).
173+
"""
174+
prompt_cfg = getattr(config, 'prompt', None)
175+
if isinstance(prompt_cfg, DictConfig):
176+
system = getattr(prompt_cfg, 'system', None)
177+
if isinstance(system, str) and system.strip():
178+
return None
179+
180+
agent = _get_prompt_agent(config)
181+
if not agent:
182+
return None
183+
184+
root_dir = _get_prompt_root_dir(config)
185+
if not root_dir:
186+
return None
187+
188+
lang, family = _get_prompt_lang_and_family(config)
189+
190+
# Language fallback: try configured lang first, then zh/en as last resort.
191+
lang_candidates = [lang]
192+
for fallback in ('zh', 'en'):
193+
if fallback not in lang_candidates:
194+
lang_candidates.append(fallback)
195+
196+
for lang_try in lang_candidates:
197+
spec = PromptFileSpec(
198+
agent=agent,
199+
lang=lang_try,
200+
family=family,
201+
root_dir=root_dir,
202+
)
203+
for path in spec.candidate_paths():
204+
if os.path.isfile(path):
205+
with open(path, 'r', encoding='utf-8') as f:
206+
text = f.read()
207+
text = text.strip('\n')
208+
return text if text.strip() else None
209+
210+
return None
211+
212+
213+
def apply_prompt_files(config: DictConfig) -> DictConfig:
214+
"""Apply prompt file resolution onto config in-place.
215+
216+
This sets `config.prompt.system` when it's missing/empty and a matching prompt file exists.
217+
"""
218+
try:
219+
prompt_text = resolve_prompt_file(config)
220+
except Exception:
221+
# Be conservative: prompt loading must never break config loading.
222+
return config
223+
224+
if not prompt_text:
225+
return config
226+
227+
if not hasattr(config, 'prompt') or config.prompt is None:
228+
config.prompt = DictConfig({})
229+
if getattr(config.prompt, 'system', None) is None or not str(
230+
getattr(config.prompt, 'system', '')).strip():
231+
config.prompt.system = prompt_text
232+
return config

0 commit comments

Comments
 (0)