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
12 changes: 5 additions & 7 deletions .claude/skills/dingo-verify/scripts/fact_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,11 @@ def build_config(
"key": api_key,
"model": model,
"api_url": api_url,
"parameters": {
"temperature": 0,
"agent_config": {
"max_concurrent_claims": max_concurrent,
"max_iterations": 50,
"tools": tools_config,
}
"temperature": 0,
"agent_config": {
"max_concurrent_claims": max_concurrent,
"max_iterations": 50,
"tools": tools_config,
}
}
}]
Expand Down
12 changes: 5 additions & 7 deletions clawhub/scripts/fact_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,11 @@ def build_config(
"key": api_key,
"model": model,
"api_url": api_url,
"parameters": {
"temperature": 0,
"agent_config": {
"max_concurrent_claims": max_concurrent,
"max_iterations": 50,
"tools": tools_config,
}
"temperature": 0,
"agent_config": {
"max_concurrent_claims": max_concurrent,
"max_iterations": 50,
"tools": tools_config,
}
}
}]
Expand Down
3 changes: 2 additions & 1 deletion dingo/config/input_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,11 @@ class EmbeddingConfigArgs(BaseModel):


class EvaluatorLLMArgs(BaseModel):
model_config = {"extra": "allow"}

model: Optional[str] = None
key: Optional[str] = None
api_url: Optional[str] = None
parameters: Optional[dict] = None
embedding_config: Optional[EmbeddingConfigArgs] = None


Expand Down
131 changes: 102 additions & 29 deletions dingo/model/llm/agent/agent_article_fact_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,21 +343,21 @@ class ArticleFactChecker(BaseAgent):
"config": {
"key": "your-openai-api-key",
"model": "gpt-4o-mini",
"parameters": {
"agent_config": {
"max_iterations": 10,
"tools": {
"claims_extractor": {
"api_key": "your-openai-api-key",
"max_claims": 50,
"claim_types": ["factual", "institutional", "statistical", "attribution"]
},
"tavily_search": {
"api_key": "your-tavily-api-key",
"max_results": 5
},
"arxiv_search": {"max_results": 5}
}
"agent_config": {
"max_iterations": 10,
"overall_timeout": 900,
"max_concurrent_claims": 5,
"tools": {
"claims_extractor": {
"api_key": "your-openai-api-key",
"max_claims": 50,
"claim_types": ["factual", "institutional", "statistical", "attribution"]
},
"tavily_search": {
"api_key": "your-tavily-api-key",
"max_results": 5
},
"arxiv_search": {"max_results": 5}
}
}
}
Expand All @@ -372,6 +372,9 @@ class ArticleFactChecker(BaseAgent):
]
max_iterations = 10 # Allow more iterations for comprehensive checking
max_concurrent_claims = 5 # Default parallel claim verification slots
overall_timeout = 900 # 15-minute wall-clock timeout for entire evaluation
_MIN_OVERALL_TIMEOUT = 30 # Floor: 30 seconds
_MAX_OVERALL_TIMEOUT = 7200 # Ceiling: 2 hours

_required_fields = [RequiredField.CONTENT] # Article text

Expand All @@ -394,8 +397,8 @@ def _get_output_dir(cls) -> Optional[str]:
Returns:
Output directory path (created if needed), or None if saving is disabled.
"""
params = cls.dynamic_config.parameters or {}
agent_cfg = params.get('agent_config') or {}
extra_params = cls.dynamic_config.model_extra
agent_cfg = extra_params.get('agent_config') or {}

explicit_path = agent_cfg.get('output_path')
if explicit_path:
Expand Down Expand Up @@ -816,24 +819,42 @@ def eval(cls, input_data: Data) -> EvalDetail:
output_dir = cls._get_output_dir()

if cls.dynamic_config:
if cls.dynamic_config.parameters is None:
cls.dynamic_config.parameters = {}
cls.dynamic_config.parameters.setdefault("temperature", 0)
if 'temperature' not in cls.dynamic_config.model_extra:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Accessing model_extra directly can raise a TypeError if no extra fields were provided during initialization, as Pydantic v2 sets model_extra to None in that case. It is safer to use (cls.dynamic_config.model_extra or {}).

Suggested change
if 'temperature' not in cls.dynamic_config.model_extra:
if 'temperature' not in (cls.dynamic_config.model_extra or {}):

cls.dynamic_config.temperature = 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Modifying cls.dynamic_config directly is not thread-safe and leads to state leakage across different evaluation tasks because dynamic_config is a class attribute shared by all instances of the evaluator. Consider using a local copy of the parameters for the current evaluation context.


if output_dir and input_data.content:
cls._save_article_content(output_dir, input_data.content)

timeout = cls._get_overall_timeout()

async def _run_with_timeout() -> EvalDetail:
return await asyncio.wait_for(
cls._async_eval(input_data, start_time, output_dir),
timeout=timeout,
)

try:
return asyncio.run(cls._async_eval(input_data, start_time, output_dir))
return asyncio.run(_run_with_timeout())
except asyncio.TimeoutError:
elapsed = time.time() - start_time
log.warning(f"ArticleFactChecker: overall timeout exceeded ({elapsed:.1f}s / {timeout:.0f}s limit)")
return cls._create_overall_timeout_result(elapsed, timeout)
except RuntimeError as e:
# Fallback when called inside an already-running event loop (e.g. Jupyter, tests)
if "cannot run" in str(e).lower() or "already running" in str(e).lower():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(
lambda: asyncio.run(cls._async_eval(input_data, start_time, output_dir))
)
return future.result()
future = pool.submit(lambda: asyncio.run(_run_with_timeout()))
try:
# Extra margin so asyncio.wait_for fires before this outer timeout
return future.result(timeout=timeout + 30)
except (asyncio.TimeoutError, concurrent.futures.TimeoutError):
elapsed = time.time() - start_time
log.warning(
f"ArticleFactChecker: overall timeout exceeded "
f"({elapsed:.1f}s / {timeout:.0f}s limit, fallback path)"
)
return cls._create_overall_timeout_result(elapsed, timeout)
raise

# --- Two-Phase Async Architecture Methods ---
Expand Down Expand Up @@ -922,8 +943,8 @@ async def _async_extract_claims(cls, input_data: Data) -> List[Dict]:
"""
from dingo.model.llm.agent.tools.claims_extractor import ClaimsExtractor, ClaimsExtractorConfig

params = cls.dynamic_config.parameters or {}
agent_cfg = params.get('agent_config') or {}
extra_params = cls.dynamic_config.model_extra
agent_cfg = extra_params.get('agent_config') or {}
extractor_cfg = agent_cfg.get('tools', {}).get('claims_extractor', {})

config_kwargs: Dict[str, Any] = {
Expand Down Expand Up @@ -1019,10 +1040,30 @@ async def _async_verify_single_claim(
@classmethod
def _get_max_concurrent_claims(cls) -> int:
"""Read max_concurrent_claims from agent_config or use class default."""
params = cls.dynamic_config.parameters or {}
agent_cfg = params.get('agent_config') or {}
extra_params = cls.dynamic_config.model_extra
agent_cfg = extra_params.get('agent_config') or {}
return agent_cfg.get('max_concurrent_claims', cls.max_concurrent_claims)

@classmethod
def _get_overall_timeout(cls) -> float:
"""Read overall_timeout from agent_config or use class default (900s).

Returns:
Positive timeout in seconds, clamped to [30, 7200].
"""
extra_params = cls.dynamic_config.model_extra
agent_cfg = extra_params.get('agent_config') or {}
raw = agent_cfg.get('overall_timeout', cls.overall_timeout)
try:
timeout = float(raw)
except (TypeError, ValueError):
log.warning(f"Invalid overall_timeout={raw!r}, using default {cls.overall_timeout}s")
return float(cls.overall_timeout)
clamped = max(cls._MIN_OVERALL_TIMEOUT, min(timeout, cls._MAX_OVERALL_TIMEOUT))
if clamped != timeout:
log.warning(f"overall_timeout={timeout} out of range, clamped to {clamped}s")
return float(clamped)

@classmethod
def _parse_claim_json_robust(cls, output: Optional[str]) -> Dict[str, Any]:
"""
Expand Down Expand Up @@ -1795,6 +1836,38 @@ def _create_error_result(cls, error_message: str) -> EvalDetail:
]
return result

@classmethod
def _create_overall_timeout_result(cls, elapsed: float, timeout: float) -> EvalDetail:
"""
Create error result when overall wall-clock timeout is exceeded.

Args:
elapsed: Actual elapsed time in seconds
timeout: Configured timeout limit in seconds

Returns:
EvalDetail with timeout error status
"""
minutes, seconds = divmod(int(timeout), 60)
limit_str = f"{minutes}m{seconds}s" if minutes else f"{int(timeout)}s"
result = EvalDetail(metric=cls.__name__)
result.status = True
result.label = [f"{QualityLabel.QUALITY_BAD_PREFIX}AGENT_OVERALL_TIMEOUT"]
result.reason = [
"Article Fact-Checking Failed: Overall Timeout Exceeded",
"=" * 70,
f"Execution exceeded the {int(timeout)}s ({limit_str}) wall-clock limit.",
f"Elapsed time: {elapsed:.1f}s",
"",
"Recommendations:",
f" 1. Increase overall_timeout (current: {int(timeout)}s) in agent_config",
" 2. Reduce max_claims in claims_extractor config (e.g., 50 -> 20)",
" 3. Use a faster model (e.g., gpt-4o-mini instead of gpt-4o)",
" 4. Reduce max_concurrent_claims to lower API rate-limit pressure",
" 5. Split long articles into shorter sections",
]
return result

@classmethod
def plan_execution(cls, input_data: Data) -> List[Dict[str, Any]]:
"""
Expand Down
16 changes: 7 additions & 9 deletions dingo/model/llm/agent/agent_fact_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,13 @@ class AgentFactCheck(BaseAgent):
"key": "your-openai-api-key",
"api_url": "https://api.openai.com/v1",
"model": "gpt-4.1-mini-2025-04-14",
"parameters": {
"agent_config": {
"max_iterations": 5,
"tools": {
"tavily_search": {
"api_key": "your-tavily-api-key",
"max_results": 5,
"search_depth": "advanced"
}
"agent_config": {
"max_iterations": 5,
"tools": {
"tavily_search": {
"api_key": "your-tavily-api-key",
"max_results": 5,
"search_depth": "advanced"
}
}
}
Expand Down
16 changes: 7 additions & 9 deletions dingo/model/llm/agent/agent_hallucination.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,13 @@ class AgentHallucination(BaseAgent):
"key": "your-openai-api-key",
"api_url": "https://api.openai.com/v1",
"model": "gpt-4.1-mini-2025-04-14",
"parameters": {
"agent_config": {
"max_iterations": 3,
"tools": {
"tavily_search": {
"api_key": "your-tavily-api-key",
"max_results": 5,
"search_depth": "advanced"
}
"agent_config": {
"max_iterations": 3,
"tools": {
"tavily_search": {
"api_key": "your-tavily-api-key",
"max_results": 5,
"search_depth": "advanced"
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions dingo/model/llm/agent/agent_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,22 +327,22 @@ def get_openai_llm_from_dingo_config(dynamic_config):
)

# Extract parameters
params = dynamic_config.parameters or {}
extra_params = dynamic_config.model_extra

# Create ChatOpenAI instance
llm = ChatOpenAI(
api_key=dynamic_config.key,
base_url=dynamic_config.api_url,
model=dynamic_config.model or "gpt-4.1-mini",
temperature=params.get("temperature", 0.3),
max_tokens=params.get("max_tokens", 4096),
top_p=params.get("top_p", 1.0),
timeout=params.get("timeout", 30)
temperature=extra_params.get("temperature", 0.3),
max_tokens=extra_params.get("max_tokens", 4096),
top_p=extra_params.get("top_p", 1.0),
timeout=extra_params.get("timeout", 30)
)

log.debug(
f"Created ChatOpenAI: model={dynamic_config.model}, "
f"temp={params.get('temperature', 0.3)}"
f"temp={extra_params.get('temperature', 0.3)}"
)

return llm
10 changes: 5 additions & 5 deletions dingo/model/llm/agent/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,16 @@ def get_tool_config(cls, tool_name: str) -> Dict[str, Any]:
Extract tool configuration from agent's dynamic_config.

Configuration is expected in:
dynamic_config.parameters.agent_config.tools.{tool_name}
dynamic_config.agent_config.tools.{tool_name}

Args:
tool_name: Name of the tool

Returns:
Dict of configuration values for the tool
"""
params = cls.dynamic_config.parameters or {}
agent_config = params.get('agent_config', {})
extra_params = cls.dynamic_config.model_extra
agent_config = extra_params.get('agent_config', {})
tools_config = agent_config.get('tools', {})
return tools_config.get(tool_name, {})

Expand Down Expand Up @@ -184,8 +184,8 @@ def get_max_iterations(cls) -> int:
Returns:
Maximum number of iterations allowed
"""
params = cls.dynamic_config.parameters or {}
agent_config = params.get('agent_config', {})
extra_params = cls.dynamic_config.model_extra
agent_config = extra_params.get('agent_config', {})
return agent_config.get('max_iterations', cls.max_iterations)

@classmethod
Expand Down
12 changes: 4 additions & 8 deletions dingo/model/llm/base_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,18 @@ def send_messages(cls, messages: List):
else:
model_name = cls.client.models.list().data[0].id

params = cls.dynamic_config.parameters
cls.validate_config(params)
extra_params = cls.dynamic_config.model_extra
cls.validate_config(extra_params)

completions = cls.client.chat.completions.create(
model=model_name,
messages=messages,
temperature=params.get("temperature", 0.3) if params else 0.3,
top_p=params.get("top_p", 1) if params else 1,
max_tokens=params.get("max_tokens", 4000) if params else 4000,
presence_penalty=params.get("presence_penalty", 0) if params else 0,
frequency_penalty=params.get("frequency_penalty", 0) if params else 0,
**extra_params,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Unpacking extra_params with ** will raise a TypeError if extra_params is None. In Pydantic v2, model_extra is None if no extra fields are present. Use **(extra_params or {}) to ensure it is always a mapping.

Suggested change
**extra_params,
**(extra_params or {}),

)

if completions.choices[0].finish_reason == "length":
raise ExceedMaxTokens(
f"Exceed max tokens: {params.get('max_tokens', 4000) if params else 4000}"
f"Exceed max tokens: {extra_params.get('max_tokens', 4000)}"
)

return str(completions.choices[0].message.content)
Expand Down
Loading
Loading