File size: 6,367 Bytes
9d0fd45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
"""Native profile loader and LLM builders for the stateful ReAct agent."""

from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from frontier_agent.core.runtime.loop.model_profile import ThinkingFormat

from frontier_agent.core.llm import LLMClient

logger = logging.getLogger(__name__)

_PROFILES_DIR = Path(__file__).resolve().parent / "profiles"
_PROJECT_ROOT = Path(__file__).resolve().parents[2]

# Retired profile names kept resolvable so saved command lines, CI jobs and
# pinned ``workflow_profile:`` values don't hard-fail with FileNotFoundError
# after a rename. Mirrors ``workflows/agent_team/profile.py``.
_PROFILE_ALIASES = {
    "default": "simple",
    "keep5": "benchmark",
    "Apodex1.1-solve": "tui",
}

# An alias must not silently change what a pinned name DOES. The retired
# ``default`` profile retained every tool result (``keep_last_k: -1``) while
# ``simple`` blanks after 5, and eight ``scripts/evaluate-*.sh`` runs plus the
# ``officeqa``/``gdpval``/``apex`` sandbox defaults are still pinned to
# ``--profile default``; remapping the name alone would quietly make those runs
# incomparable with every earlier result. Applied before caller ``overrides``,
# so an explicit override still wins.
_ALIAS_OVERRIDES: dict[str, dict[str, Any]] = {
    "default": {"agent": {"keep_last_k": -1}},
}


def _deep_merge(base: dict[str, Any], overrides: dict[str, Any]) -> dict[str, Any]:
    """Recursively merge profile overrides without importing another workflow."""
    merged = dict(base)
    for key, value in overrides.items():
        if isinstance(value, dict) and isinstance(merged.get(key), dict):
            merged[key] = _deep_merge(merged[key], value)
        else:
            merged[key] = value
    return merged


def load_react_profile(
    name: str,
    *,
    overrides: dict[str, Any] | None = None,
    inline: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Load a ReAct profile YAML with env-var resolution."""
    import yaml
    from dotenv import load_dotenv

    from frontier_agent.infra.config import _resolve_env_vars

    load_dotenv(_PROJECT_ROOT / ".env", override=False)

    if inline is not None:
        raw: Any = inline
    else:
        path = _PROFILES_DIR / f"{_PROFILE_ALIASES.get(name, name)}.yaml"
        if not path.exists():
            raise FileNotFoundError(f"ReAct profile not found: {path}")
        raw = yaml.safe_load(path.read_text(encoding="utf-8"))

    resolved = _resolve_env_vars(raw)
    if inline is None and (alias_overrides := _ALIAS_OVERRIDES.get(name)):
        resolved = _deep_merge(resolved, alias_overrides)
    if overrides:
        resolved = _deep_merge(resolved, overrides)
    return resolved


def _resolve_thinking_format(profile: dict[str, Any]) -> ThinkingFormat:
    from frontier_agent.core.runtime.loop.model_profile import (
        infer_thinking_format,
        is_thinking_format,
    )
    from frontier_agent.infra.protocol_client import (
        protocol_of,
        thinking_format_for_protocol,
    )

    explicit = (profile.get("agent") or {}).get("thinking_format")
    if explicit:
        if is_thinking_format(explicit):
            return explicit
        logger.warning(
            "profile agent.thinking_format=%r is not a known format — ignoring",
            explicit,
        )
    # Native Anthropic / OpenAI Responses return content as a typed block list
    # → content_block so the parser keeps the verbatim blocks.
    by_protocol = thinking_format_for_protocol(protocol_of(profile.get("llm") or {}))
    if by_protocol:
        return by_protocol
    model_id = (profile.get("llm") or {}).get("model")
    return infer_thinking_format(model_id, default="tag")


def create_react_llm(profile: dict[str, Any]) -> LLMClient:
    """Create the ReAct LLM from a profile dict, keyed on ``llm.protocol``.

    - ``anthropic`` → native Anthropic Messages API (thinking + signature).
    - ``responses`` → OpenAI Responses API (reasoning + encrypted_content).
    - ``chat_completions`` (default) → OpenAI-compatible Chat Completions.
    """
    from frontier_agent.infra.llm import with_provider_stamp
    from frontier_agent.infra.openai_client import OpenAIClient
    from frontier_agent.infra.protocol_client import build_protocol_client

    cfg = profile["llm"]
    provider = cfg.get("_provider_label") or cfg.get("provider") or "openai"
    protocol_client = build_protocol_client(cfg, title="FrontierAgent-StatefulReAct")
    if protocol_client is not None:
        return with_provider_stamp(protocol_client, str(provider))

    extra_body: dict[str, Any] = dict(cfg.get("extra_body") or {})

    if (top_p := cfg.get("top_p")) is not None:
        extra_body.setdefault("top_p", top_p)
    repetition_penalty = cfg.get("repetition_penalty")
    if (
        repetition_penalty is not None
        and repetition_penalty != 1.0
        and "repetition_penalty" not in extra_body
    ):
        extra_body["repetition_penalty"] = repetition_penalty

    client = OpenAIClient(
        model=cfg["model"],
        api_key=cfg.get("api_key", "dummy"),
        base_url=cfg.get("base_url"),
        temperature=cfg.get("temperature", 0.0),
        # int(): ``max_tokens`` may come from a ``${OPENAI_MAX_TOKENS:-…}``
        # placeholder, and env-var substitution always yields a string. It is
        # forwarded verbatim into the request body, where a quoted number is a
        # 400 from the endpoint.
        max_completion_tokens=int(cfg.get("max_tokens") or 65536),
        default_headers={
            "HTTP-Referer": "frontier_agent",
            "X-Title": "FrontierAgent-StatefulReAct",
        },
        extra_body=extra_body or None,
    )
    return with_provider_stamp(client, str(provider))


def build_react_model_profile(profile: dict[str, Any]) -> Any:
    """Build a ModelProfile from a ReAct profile dict."""
    from frontier_agent.core.runtime.loop.model_profile import ModelProfile
    from frontier_agent.infra.protocol_client import protocol_of

    return ModelProfile(
        model_id=profile["llm"]["model"],
        provider=str(profile["llm"].get("provider") or "openai"),
        thinking_format=_resolve_thinking_format(profile),
        protocol=protocol_of(profile.get("llm") or {}),
    )