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
3 changes: 2 additions & 1 deletion config.researchclaw.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ openclaw_bridge:
llm:
provider: "openai-compatible"
base_url: "https://api.openai.com/v1"
wire_api: "chat_completions" # Set to "responses" for Responses-only gateways
api_key_env: "OPENAI_API_KEY"
api_key: ""
primary_model: "gpt-4o"
Expand Down Expand Up @@ -215,4 +216,4 @@ metaclaw_bridge:
lesson_to_skill:
enabled: true
min_severity: "error" # Only convert error-level lessons
max_skills_per_run: 3
max_skills_per_run: 3
86 changes: 60 additions & 26 deletions researchclaw/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import sys
import yaml

DEFAULT_PYTHON_PATH = ".venv/Scripts/python.exe" if sys.platform == "win32" else ".venv/bin/python3"
DEFAULT_PYTHON_PATH = (
".venv/Scripts/python.exe" if sys.platform == "win32" else ".venv/bin/python3"
)

CONFIG_SEARCH_ORDER: tuple[str, ...] = ("config.arc.yaml", "config.yaml")

Expand All @@ -32,8 +34,11 @@ def _validate_network_policy(val: object, default: str = "setup_only") -> str:
s = str(val).strip().lower() if val else default
if s not in _VALID_NETWORK_POLICIES:
import logging as _cfg_log

_cfg_log.getLogger(__name__).warning(
"Invalid network_policy %r, using %r", val, default,
"Invalid network_policy %r, using %r",
val,
default,
)
return default
return s
Expand All @@ -48,12 +53,15 @@ def _safe_float(val: Any, default: float) -> float:
return default
try:
import math

result = float(val)
if not math.isfinite(result):
return default
return result
except (ValueError, TypeError):
return default


EXAMPLE_CONFIG = "config.researchclaw.example.yaml"


Expand Down Expand Up @@ -87,7 +95,14 @@ def resolve_config_path(explicit: str | None) -> Path | None:
)
PROJECT_MODES = {"docs-first", "semi-auto", "full-auto"}
KB_BACKENDS = {"markdown", "obsidian"}
EXPERIMENT_MODES = {"simulated", "sandbox", "docker", "ssh_remote", "colab_drive", "agentic"}
EXPERIMENT_MODES = {
"simulated",
"sandbox",
"docker",
"ssh_remote",
"colab_drive",
"agentic",
}
CLI_AGENT_PROVIDERS = {"llm", "claude_code", "codex"}


Expand Down Expand Up @@ -175,6 +190,7 @@ class AcpConfig:
class LlmConfig:
provider: str
base_url: str = ""
wire_api: str = "chat_completions"
api_key_env: str = ""
api_key: str = ""
primary_model: str = ""
Expand Down Expand Up @@ -394,8 +410,8 @@ class CliAgentConfig:
"""

provider: str = "llm"
binary_path: str = "" # auto-detected via PATH if empty
model: str = "" # model override for the CLI agent
binary_path: str = "" # auto-detected via PATH if empty
model: str = "" # model override for the CLI agent
max_budget_usd: float = 5.0
timeout_sec: int = 600
extra_args: tuple[str, ...] = ()
Expand Down Expand Up @@ -484,6 +500,7 @@ class ExportConfig:
authors: str = "Anonymous"
bib_file: str = "references"


@dataclass(frozen=True)
class PromptsConfig:
"""Configuration for prompt externalization."""
Expand Down Expand Up @@ -618,7 +635,6 @@ class OverleafConfig:
poll_interval_sec: int = 300



COPILOT_MODES = ("co-pilot", "auto-pilot", "zero-touch")


Expand Down Expand Up @@ -653,7 +669,11 @@ class QualityAssessorConfig:

enabled: bool = True
dimensions: tuple[str, ...] = (
"novelty", "rigor", "clarity", "impact", "experiments"
"novelty",
"rigor",
"clarity",
"impact",
"experiments",
)
venue_recommendation: bool = True
score_history: bool = True
Expand Down Expand Up @@ -683,9 +703,7 @@ class RCConfig:
export: ExportConfig = field(default_factory=ExportConfig)
prompts: PromptsConfig = field(default_factory=PromptsConfig)
web_search: WebSearchConfig = field(default_factory=WebSearchConfig)
metaclaw_bridge: MetaClawBridgeConfig = field(
default_factory=MetaClawBridgeConfig
)
metaclaw_bridge: MetaClawBridgeConfig = field(default_factory=MetaClawBridgeConfig)
# Agent B: Intelligence & Memory
memory: MemoryConfig = field(default_factory=MemoryConfig)
skills: SkillsConfig = field(default_factory=SkillsConfig)
Expand All @@ -701,7 +719,9 @@ class RCConfig:
# Agent D: Research Enhancement
trends: TrendsConfig = field(default_factory=TrendsConfig)
copilot: CoPilotConfig = field(default_factory=CoPilotConfig)
quality_assessor: QualityAssessorConfig = field(default_factory=QualityAssessorConfig)
quality_assessor: QualityAssessorConfig = field(
default_factory=QualityAssessorConfig
)
calendar: CalendarConfig = field(default_factory=CalendarConfig)

def to_dict(self) -> dict[str, Any]:
Expand Down Expand Up @@ -807,10 +827,14 @@ def from_dict(
web_search=WebSearchConfig(
enabled=bool(web_search.get("enabled", True)),
tavily_api_key=str(web_search.get("tavily_api_key", "")),
tavily_api_key_env=str(web_search.get("tavily_api_key_env", "TAVILY_API_KEY")),
tavily_api_key_env=str(
web_search.get("tavily_api_key_env", "TAVILY_API_KEY")
),
enable_scholar=bool(web_search.get("enable_scholar", True)),
enable_crawling=bool(web_search.get("enable_crawling", True)),
enable_pdf_extraction=bool(web_search.get("enable_pdf_extraction", True)),
enable_pdf_extraction=bool(
web_search.get("enable_pdf_extraction", True)
),
max_web_results=int(web_search.get("max_web_results", 10)),
max_scholar_results=int(web_search.get("max_scholar_results", 10)),
max_crawl_urls=int(web_search.get("max_crawl_urls", 5)),
Expand Down Expand Up @@ -881,6 +905,13 @@ def validate_config(
if not _is_blank(kb_backend) and kb_backend not in KB_BACKENDS:
errors.append(f"Invalid knowledge_base.backend: {kb_backend}")

llm_wire_api = _get_by_path(data, "llm.wire_api")
if not _is_blank(llm_wire_api) and llm_wire_api not in (
"chat_completions",
"responses",
):
errors.append(f"Invalid llm.wire_api: {llm_wire_api}")

hitl_required_stages = _get_by_path(data, "security.hitl_required_stages")
if hitl_required_stages is not None:
if not isinstance(hitl_required_stages, list):
Expand All @@ -901,7 +932,10 @@ def validate_config(
errors.append(f"Invalid experiment.metric_direction: {exp_direction}")

cli_agent_provider = _get_by_path(data, "experiment.cli_agent.provider")
if not _is_blank(cli_agent_provider) and cli_agent_provider not in CLI_AGENT_PROVIDERS:
if (
not _is_blank(cli_agent_provider)
and cli_agent_provider not in CLI_AGENT_PROVIDERS
):
errors.append(f"Invalid experiment.cli_agent.provider: {cli_agent_provider}")

kb_root_raw = _get_by_path(data, "knowledge_base.root")
Expand All @@ -925,6 +959,7 @@ def _parse_llm_config(data: dict[str, Any]) -> LlmConfig:
return LlmConfig(
provider=data.get("provider", "openai-compatible"),
base_url=data.get("base_url", ""),
wire_api=data.get("wire_api", "chat_completions"),
api_key_env=data.get("api_key_env", ""),
api_key=data.get("api_key", ""),
primary_model=data.get("primary_model", ""),
Expand Down Expand Up @@ -984,9 +1019,7 @@ def _parse_experiment_config(data: dict[str, Any]) -> ExperimentConfig:
docker=DockerSandboxConfig(
image=docker_data.get("image", "researchclaw/experiment:latest"),
gpu_enabled=bool(docker_data.get("gpu_enabled", True)),
gpu_device_ids=tuple(
int(g) for g in docker_data.get("gpu_device_ids", ())
),
gpu_device_ids=tuple(int(g) for g in docker_data.get("gpu_device_ids", ())),
memory_limit_mb=_safe_int(docker_data.get("memory_limit_mb"), 8192),
network_policy=_validate_network_policy(
docker_data.get("network_policy", "setup_only"),
Expand All @@ -1013,7 +1046,9 @@ def _parse_experiment_config(data: dict[str, Any]) -> ExperimentConfig:
docker_network_policy=_validate_network_policy(
ssh_data.get("docker_network_policy", "none"),
),
docker_memory_limit_mb=_safe_int(ssh_data.get("docker_memory_limit_mb"), 8192),
docker_memory_limit_mb=_safe_int(
ssh_data.get("docker_memory_limit_mb"), 8192
),
docker_shm_size_mb=_safe_int(ssh_data.get("docker_shm_size_mb"), 2048),
timeout_sec=_safe_int(ssh_data.get("timeout_sec"), 600),
scp_timeout_sec=_safe_int(ssh_data.get("scp_timeout_sec"), 300),
Expand Down Expand Up @@ -1065,9 +1100,7 @@ def _parse_figure_agent_config(data: dict[str, Any]) -> FigureAgentConfig:
max_figures=_safe_int(data.get("max_figures"), 8),
max_iterations=_safe_int(data.get("max_iterations"), 3),
render_timeout_sec=_safe_int(data.get("render_timeout_sec"), 30),
use_docker=(
None if use_docker_raw is None else bool(use_docker_raw)
),
use_docker=(None if use_docker_raw is None else bool(use_docker_raw)),
docker_image=data.get("docker_image", "researchclaw/experiment:latest"),
output_format=data.get("output_format", "python"),
gemini_api_key=data.get("gemini_api_key", ""),
Expand Down Expand Up @@ -1104,7 +1137,6 @@ def _parse_cli_agent_config(data: dict[str, Any]) -> CliAgentConfig:
)



def _parse_code_agent_config(data: dict[str, Any]) -> CodeAgentConfig:
if not data:
return CodeAgentConfig()
Expand All @@ -1113,7 +1145,9 @@ def _parse_code_agent_config(data: dict[str, Any]) -> CodeAgentConfig:
architecture_planning=bool(data.get("architecture_planning", True)),
sequential_generation=bool(data.get("sequential_generation", True)),
hard_validation=bool(data.get("hard_validation", True)),
hard_validation_max_repairs=_safe_int(data.get("hard_validation_max_repairs"), 4),
hard_validation_max_repairs=_safe_int(
data.get("hard_validation_max_repairs"), 4
),
exec_fix_max_iterations=_safe_int(data.get("exec_fix_max_iterations"), 3),
exec_fix_timeout_sec=_safe_int(data.get("exec_fix_timeout_sec"), 60),
tree_search_enabled=bool(data.get("tree_search_enabled", False)),
Expand Down Expand Up @@ -1304,8 +1338,6 @@ def _parse_dashboard_config(data: dict[str, Any]) -> DashboardConfig:
)




def _parse_trends_config(data: dict[str, Any]) -> TrendsConfig:
if not data:
return TrendsConfig()
Expand Down Expand Up @@ -1342,7 +1374,9 @@ def _parse_copilot_config(data: dict[str, Any]) -> CoPilotConfig:
def _parse_quality_assessor_config(data: dict[str, Any]) -> QualityAssessorConfig:
if not data:
return QualityAssessorConfig()
dimensions = data.get("dimensions", ("novelty", "rigor", "clarity", "impact", "experiments"))
dimensions = data.get(
"dimensions", ("novelty", "rigor", "clarity", "impact", "experiments")
)
if isinstance(dimensions, list):
dimensions = tuple(dimensions)
return QualityAssessorConfig(
Expand Down
Loading