Commit ·
0c9c31a
1
Parent(s): 7f8f0e0
Gemini vision (solves chess), solver plain-text fallback, pandas in python_repl
Browse filesVision provider configurable, default Gemini (reads the chess board -> Rd5). Solver falls back to plain-text when structured output returns empty (was dropping the Excel question). python_repl preloads pandas as pd and prompts push numeric work through it. HF_TOKEN documented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .env.example +3 -0
- gaia_agent/config.py +4 -0
- gaia_agent/llm.py +29 -3
- gaia_agent/nodes.py +18 -9
- gaia_agent/prompts.py +4 -2
- gaia_agent/tools/python_tool.py +13 -3
.env.example
CHANGED
|
@@ -11,6 +11,9 @@ HF_TOKEN=
|
|
| 11 |
# "openai_compatible" (freellmapi gateway, best for rate limits), "gemini", "groq".
|
| 12 |
LLM_PROVIDER=gemini
|
| 13 |
GEMINI_TEXT_MODEL=gemini-2.5-flash
|
|
|
|
|
|
|
|
|
|
| 14 |
GROQ_TEXT_MODEL=llama-3.1-8b-instant
|
| 15 |
# Global pace across all text-LLM calls (req/sec). 0.15 = ~9/min (<10 RPM free).
|
| 16 |
# Ignored when LLM_PROVIDER=openai_compatible (gateway manages its own limits).
|
|
|
|
| 11 |
# "openai_compatible" (freellmapi gateway, best for rate limits), "gemini", "groq".
|
| 12 |
LLM_PROVIDER=gemini
|
| 13 |
GEMINI_TEXT_MODEL=gemini-2.5-flash
|
| 14 |
+
# Image understanding: "gemini" (best), "groq", or "openai_compatible"
|
| 15 |
+
VISION_PROVIDER=gemini
|
| 16 |
+
GEMINI_VISION_MODEL=gemini-2.0-flash
|
| 17 |
GROQ_TEXT_MODEL=llama-3.1-8b-instant
|
| 18 |
# Global pace across all text-LLM calls (req/sec). 0.15 = ~9/min (<10 RPM free).
|
| 19 |
# Ignored when LLM_PROVIDER=openai_compatible (gateway manages its own limits).
|
gaia_agent/config.py
CHANGED
|
@@ -28,6 +28,10 @@ class Settings(BaseSettings):
|
|
| 28 |
# Vision + Whisper always use Groq.
|
| 29 |
llm_provider: str = "gemini"
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
# --- Models (override via env) ---
|
| 32 |
gemini_text_model: str = "gemini-2.5-flash"
|
| 33 |
groq_text_model: str = "llama-3.1-8b-instant"
|
|
|
|
| 28 |
# Vision + Whisper always use Groq.
|
| 29 |
llm_provider: str = "gemini"
|
| 30 |
|
| 31 |
+
# --- Vision (image understanding) provider: "gemini", "groq", or "openai_compatible" ---
|
| 32 |
+
vision_provider: str = "gemini"
|
| 33 |
+
gemini_vision_model: str = "gemini-2.0-flash"
|
| 34 |
+
|
| 35 |
# --- Models (override via env) ---
|
| 36 |
gemini_text_model: str = "gemini-2.5-flash"
|
| 37 |
groq_text_model: str = "llama-3.1-8b-instant"
|
gaia_agent/llm.py
CHANGED
|
@@ -87,14 +87,40 @@ def get_structured_llm(schema, temperature: float = 0.0):
|
|
| 87 |
|
| 88 |
@lru_cache(maxsize=1)
|
| 89 |
def get_vision_llm():
|
| 90 |
-
"""Return the
|
| 91 |
-
from langchain_groq import ChatGroq
|
| 92 |
|
|
|
|
|
|
|
|
|
|
| 93 |
s = get_settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
return ChatGroq(
|
| 95 |
model=s.groq_vision_model,
|
| 96 |
api_key=s.groq_api_key,
|
| 97 |
temperature=0.0,
|
| 98 |
max_retries=2,
|
| 99 |
-
rate_limiter=_rate_limiter(),
|
| 100 |
)
|
|
|
|
| 87 |
|
| 88 |
@lru_cache(maxsize=1)
|
| 89 |
def get_vision_llm():
|
| 90 |
+
"""Return the multimodal model used for image understanding.
|
|
|
|
| 91 |
|
| 92 |
+
Defaults to Gemini (strongest vision); falls back to Groq if no Google key.
|
| 93 |
+
Image questions are rare (1-2/run) so a low-quota key is fine here.
|
| 94 |
+
"""
|
| 95 |
s = get_settings()
|
| 96 |
+
vp = s.vision_provider.lower()
|
| 97 |
+
|
| 98 |
+
if vp == "gemini" and s.google_api_key:
|
| 99 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 100 |
+
|
| 101 |
+
return ChatGoogleGenerativeAI(
|
| 102 |
+
model=s.gemini_vision_model,
|
| 103 |
+
google_api_key=s.google_api_key,
|
| 104 |
+
temperature=0.0,
|
| 105 |
+
max_retries=2,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
if vp == "openai_compatible" and s.openai_compatible_base_url:
|
| 109 |
+
from langchain_openai import ChatOpenAI
|
| 110 |
+
|
| 111 |
+
return ChatOpenAI(
|
| 112 |
+
model=s.openai_compatible_model,
|
| 113 |
+
base_url=s.openai_compatible_base_url,
|
| 114 |
+
api_key=s.openai_compatible_api_key,
|
| 115 |
+
temperature=0.0,
|
| 116 |
+
max_retries=2,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
from langchain_groq import ChatGroq
|
| 120 |
+
|
| 121 |
return ChatGroq(
|
| 122 |
model=s.groq_vision_model,
|
| 123 |
api_key=s.groq_api_key,
|
| 124 |
temperature=0.0,
|
| 125 |
max_retries=2,
|
|
|
|
| 126 |
)
|
gaia_agent/nodes.py
CHANGED
|
@@ -144,18 +144,27 @@ def route_after_research(state: GraphState) -> str:
|
|
| 144 |
|
| 145 |
def solver(state: GraphState) -> dict:
|
| 146 |
"""Synthesize the research conversation + context into one Candidate answer."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
try:
|
| 148 |
-
|
| 149 |
-
system = SOLVER_PROMPT.format(
|
| 150 |
-
rules=GAIA_RULES,
|
| 151 |
-
question=state["question"],
|
| 152 |
-
context_notes=state.get("context_notes", "") or "(none)",
|
| 153 |
-
)
|
| 154 |
-
cand = llm.invoke([SystemMessage(system), *state.get("messages", [])])
|
| 155 |
except Exception: # noqa: BLE001
|
| 156 |
cand = None
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
return {"candidate": cand}
|
| 160 |
|
| 161 |
|
|
|
|
| 144 |
|
| 145 |
def solver(state: GraphState) -> dict:
|
| 146 |
"""Synthesize the research conversation + context into one Candidate answer."""
|
| 147 |
+
system = SOLVER_PROMPT.format(
|
| 148 |
+
rules=GAIA_RULES,
|
| 149 |
+
question=state["question"],
|
| 150 |
+
context_notes=state.get("context_notes", "") or "(none)",
|
| 151 |
+
)
|
| 152 |
+
msgs = [SystemMessage(system), *state.get("messages", [])]
|
| 153 |
try:
|
| 154 |
+
cand = get_structured_llm(Candidate).invoke(msgs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
except Exception: # noqa: BLE001
|
| 156 |
cand = None
|
| 157 |
+
|
| 158 |
+
# Structured output sometimes returns None/empty (model emits no function call).
|
| 159 |
+
# Fall back to a plain-text answer so we never drop a solvable question.
|
| 160 |
+
if cand is None or not (cand.answer or "").strip():
|
| 161 |
+
try:
|
| 162 |
+
txt = get_text_llm().invoke(
|
| 163 |
+
msgs + [HumanMessage("Output ONLY the final answer, nothing else.")]
|
| 164 |
+
).content
|
| 165 |
+
cand = Candidate(answer=(txt or "").strip(), justification="plain-fallback")
|
| 166 |
+
except Exception: # noqa: BLE001
|
| 167 |
+
cand = Candidate(answer="", justification="solver-fallback")
|
| 168 |
return {"candidate": cand}
|
| 169 |
|
| 170 |
|
gaia_agent/prompts.py
CHANGED
|
@@ -32,8 +32,10 @@ file, attachment, image, audio, spreadsheet, table, or document, set needs_file=
|
|
| 32 |
|
| 33 |
RESEARCH_PROMPT = """\
|
| 34 |
You are the RESEARCHER. Gather the facts needed to answer the question precisely.
|
| 35 |
-
Use the available tools. Prefer authoritative sources.
|
| 36 |
-
|
|
|
|
|
|
|
| 37 |
|
| 38 |
{rules}
|
| 39 |
|
|
|
|
| 32 |
|
| 33 |
RESEARCH_PROMPT = """\
|
| 34 |
You are the RESEARCHER. Gather the facts needed to answer the question precisely.
|
| 35 |
+
Use the available tools. Prefer authoritative sources. NEVER do arithmetic in your head
|
| 36 |
+
-- for ANY counting, summing, or numeric work call python_repl (pandas is available as
|
| 37 |
+
`pd`; read attached spreadsheets with pd.read_excel(path) using the file path in context).
|
| 38 |
+
Stop calling tools once you have enough.
|
| 39 |
|
| 40 |
{rules}
|
| 41 |
|
gaia_agent/tools/python_tool.py
CHANGED
|
@@ -23,9 +23,11 @@ _SAFE_BUILTINS = {
|
|
| 23 |
def python_repl(code: str) -> str:
|
| 24 |
"""Execute a short Python snippet and return its stdout (and `result` if set).
|
| 25 |
|
| 26 |
-
Use for exact arithmetic, string manipulation, sorting, and date math.
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
| 29 |
|
| 30 |
Args:
|
| 31 |
code: Python source to execute.
|
|
@@ -45,6 +47,14 @@ def python_repl(code: str) -> str:
|
|
| 45 |
"itertools": itertools,
|
| 46 |
"collections": collections,
|
| 47 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
buf = io.StringIO()
|
| 49 |
try:
|
| 50 |
with contextlib.redirect_stdout(buf):
|
|
|
|
| 23 |
def python_repl(code: str) -> str:
|
| 24 |
"""Execute a short Python snippet and return its stdout (and `result` if set).
|
| 25 |
|
| 26 |
+
Use for exact arithmetic, string manipulation, sorting, and date math. ALWAYS use
|
| 27 |
+
this for numeric work instead of computing in your head. ``math``, ``statistics``,
|
| 28 |
+
``datetime``, ``re``, ``itertools``, ``collections`` and ``pd`` (pandas) are
|
| 29 |
+
available -- e.g. read a spreadsheet with ``pd.read_excel(path)``. Assign to a
|
| 30 |
+
variable named ``result`` to return a value.
|
| 31 |
|
| 32 |
Args:
|
| 33 |
code: Python source to execute.
|
|
|
|
| 47 |
"itertools": itertools,
|
| 48 |
"collections": collections,
|
| 49 |
}
|
| 50 |
+
# Preload pandas as `pd` so spreadsheet math is exact, e.g.
|
| 51 |
+
# df = pd.read_excel(path); result = df[["Burgers","Fries"]].sum().sum()
|
| 52 |
+
try:
|
| 53 |
+
import pandas as pd
|
| 54 |
+
|
| 55 |
+
env["pd"] = pd
|
| 56 |
+
except Exception: # noqa: BLE001
|
| 57 |
+
pass
|
| 58 |
buf = io.StringIO()
|
| 59 |
try:
|
| 60 |
with contextlib.redirect_stdout(buf):
|