Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ apps/agentfabric/config/local_user/*
ast_index_file.py


#neo4j
# neo4j
.neo4j.lock
neo4j.lock
/temp/
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ For more details, please refer to [**MS-Agent Skills**](ms_agent/skill/README.md
---


### Agentic Insight
### Agentic Insight (Deep Research)

#### - Lightweight, Efficient, and Extensible Multi-modal Deep Research Framework

Expand Down
3 changes: 1 addition & 2 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,7 @@ asyncio.run(main())

---


### Agentic Insight
### Agentic Insight (Deep Research)

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

Expand Down
9 changes: 9 additions & 0 deletions ms_agent/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from copy import deepcopy
from typing import Any, Dict, Union

from ms_agent.prompting import apply_prompt_files
from ms_agent.utils import get_logger
from omegaconf import DictConfig, ListConfig, OmegaConf
from omegaconf.basecontainer import BaseContainer
Expand Down Expand Up @@ -95,6 +96,14 @@ def from_task(cls,
config.local_dir = config_dir_or_id
config.name = name
config = cls.fill_missing_fields(config)
# Prompt files: resolve config.prompt.system from prompts/ directory
# if user didn't specify inline prompt.system.
try:
if isinstance(config, DictConfig):
config = apply_prompt_files(config)
except Exception:
# Never block config loading due to prompt resolving.
pass
Comment on lines +104 to +106
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a broad Exception and then passing can hide potential issues during prompt file resolution. It's generally better to catch more specific exceptions or at least log the exception for debugging purposes, even if the intention is to not block config loading.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

或者记录日志 logger.warning(f'Prompt resolution failed: {e}')

return config

@staticmethod
Expand Down
2 changes: 2 additions & 0 deletions ms_agent/prompting/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
from .file_resolver import apply_prompt_files, resolve_prompt_file
232 changes: 232 additions & 0 deletions ms_agent/prompting/file_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
import os
from dataclasses import dataclass
from typing import List, Optional, Tuple

from omegaconf import DictConfig


@dataclass(frozen=True)
class PromptFileSpec:
agent: str
lang: str
family: str
root_dir: str

def candidate_paths(self) -> List[str]:
"""Return candidate prompt file paths in priority order."""
# File convention: prompts/{agent}/{lang}/{family}.md
# Fallback: family -> base
agent = self.agent.strip()
lang = self.lang.strip()
family = self.family.strip()
root = self.root_dir

paths = []
if family:
paths.extend([
os.path.join(root, agent, lang, f'{family}.txt'),
os.path.join(root, agent, lang, f'{family}.md'),
])
# base fallback
paths.extend([
os.path.join(root, agent, lang, 'base.txt'),
os.path.join(root, agent, lang, 'base.md')
])
return paths


def _norm_lang(lang: Optional[str]) -> str:
if not lang:
return 'zh'
lang = str(lang).strip().lower()
if lang in {'zh', 'zh-cn', 'zh_cn', 'cn'}:
return 'zh'
if lang in {'en', 'en-us', 'en_us', 'us'}:
return 'en'
if lang == 'auto':
# We cannot reliably detect user language at config-load time,
# so treat "auto" as default language (with env override handled elsewhere).
return 'zh'
return lang


def _infer_family_from_model(model: Optional[str]) -> str:
"""Infer a reasonable prompt family name from model string.

Notes:
- This is a best-effort heuristic to keep user onboarding simple.
- Users can always override via `prompt.family`.
"""
if not model:
return 'base'
m = str(model).strip().lower()

# Qwen series
if 'qwen' in m:
# Common variants: qwen3-*, qwen-3, qwen2.5-*, Qwen/Qwen3-...
if 'qwen3' in m or 'qwen-3' in m or 'qwen/qwen3' in m:
return 'qwen-3'
if 'qwen2' in m or 'qwen-2' in m:
return 'qwen-2'
if 'qwen1' in m or 'qwen-1' in m:
return 'qwen-1'
return 'qwen'

# Claude series
if 'claude' in m:
return 'claude'

# GPT-like series (OpenAI / compatible)
if 'gpt' in m or m.startswith('o1') or m.startswith('o3'):
return 'gpt'

return 'base'


def _get_prompt_root_dir(config: DictConfig) -> Optional[str]:
"""Resolve prompts root directory.

Priority:
- config.prompt.root (absolute or relative to config.local_dir)
- <config.local_dir>/prompts
"""
local_dir = getattr(config, 'local_dir', None)
prompt_cfg = getattr(config, 'prompt', None)
root = None
if isinstance(prompt_cfg, DictConfig):
root = getattr(prompt_cfg, 'root', None)

if root:
root = str(root).strip()
if not root:
root = None
elif not os.path.isabs(root) and local_dir:
root = os.path.join(str(local_dir), root)

if not root and local_dir:
root = os.path.join(str(local_dir), 'prompts')

return root


def _get_prompt_agent(config: DictConfig) -> Optional[str]:
"""Resolve agent name used in prompts/{agent}/... path."""
prompt_cfg = getattr(config, 'prompt', None)
if isinstance(prompt_cfg, DictConfig):
agent = getattr(prompt_cfg, 'agent', None)
if agent:
agent = str(agent).strip()
if agent:
return agent

# Prefer `code_file` for project agents (deep_research v2 uses this)
code_file = getattr(config, 'code_file', None)
if code_file:
code_file = str(code_file).strip()
if code_file:
return code_file

# Fallback: try `tag` (may be too specific; we only use it if user opts in via prompt.agent)
return None


def _get_prompt_lang_and_family(config: DictConfig) -> Tuple[str, str]:
prompt_cfg = getattr(config, 'prompt', None)

# lang
env_lang = os.environ.get('MS_AGENT_PROMPT_LANG') or os.environ.get(
'MS_AGENT_LANG')
cfg_lang = getattr(prompt_cfg, 'lang', None) if isinstance(
prompt_cfg, DictConfig) else None
lang = _norm_lang(cfg_lang or env_lang or 'zh')

# family
env_family = os.environ.get('MS_AGENT_PROMPT_FAMILY')
cfg_family = getattr(prompt_cfg, 'family', None) if isinstance(
prompt_cfg, DictConfig) else None

family = (cfg_family or env_family or 'auto')
family = str(family).strip()
if not family:
family = 'auto'
if family.lower() == 'auto':
model = None
if hasattr(config, 'llm') and getattr(config, 'llm') is not None:
try:
model = getattr(config.llm, 'model', None)
except Exception:
model = None
family = _infer_family_from_model(model)
return lang, family


def resolve_prompt_file(config: DictConfig) -> Optional[str]:
"""Resolve system prompt text from prompt files.

Returns:
Prompt text if a file is found, else None.

Compatibility rules:
- If `prompt.system` exists and is non-empty, this resolver is NOT used.
- Resolver is only eligible when we can infer a prompt agent name (or user provided prompt.agent).
"""
prompt_cfg = getattr(config, 'prompt', None)
if isinstance(prompt_cfg, DictConfig):
system = getattr(prompt_cfg, 'system', None)
if isinstance(system, str) and system.strip():
return None

agent = _get_prompt_agent(config)
if not agent:
return None

root_dir = _get_prompt_root_dir(config)
if not root_dir:
return None

lang, family = _get_prompt_lang_and_family(config)

# Language fallback: try configured lang first, then zh/en as last resort.
lang_candidates = [lang]
for fallback in ('zh', 'en'):
if fallback not in lang_candidates:
lang_candidates.append(fallback)

for lang_try in lang_candidates:
spec = PromptFileSpec(
agent=agent,
lang=lang_try,
family=family,
root_dir=root_dir,
)
for path in spec.candidate_paths():
if os.path.isfile(path):
with open(path, 'r', encoding='utf-8') as f:
text = f.read()
text = text.strip('\n')
return text if text.strip() else None

return None


def apply_prompt_files(config: DictConfig) -> DictConfig:
"""Apply prompt file resolution onto config in-place.

This sets `config.prompt.system` when it's missing/empty and a matching prompt file exists.
"""
try:
prompt_text = resolve_prompt_file(config)
except Exception:
# Be conservative: prompt loading must never break config loading.
return config
Comment on lines +220 to +222
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the issue in ms_agent/config/config.py, catching a broad Exception and then returning config can mask underlying problems during prompt file resolution. Consider logging the exception to aid debugging if unexpected issues arise.


if not prompt_text:
return config

if not hasattr(config, 'prompt') or config.prompt is None:
config.prompt = DictConfig({})
if getattr(config.prompt, 'system', None) is None or not str(
getattr(config.prompt, 'system', '')).strip():
config.prompt.system = prompt_text
return config
Loading
Loading