Agent Eval Tests

May 27, 2026 ยท View on GitHub

The suites under services/tests/eval/ are service-owned, daemon-backed functional evals for the LLM agent path. They do not launch the GUI. Each case starts a temporary airunner_services.daemon, sends one request to /llm/generate, and asserts a stable workflow signal.

flowchart LR
    A[pytest case] --> B[started_daemon]
    B --> C[POST /llm/generate]
    C --> D{Assertion surface}
    D --> E[visible reply]
    D --> F[tool names or daemon log]
    D --> G[LLM as judge scores]

Running The Suite

Prerequisites:

  • Activate the project virtual environment.
  • Ensure the target model artifacts already exist locally.
  • Expect clean skips when a required model artifact is missing.

Recommended commands:

# Run the full agent-eval directory.
AIRUNNER_TEST_NO_GUI_LAUNCH=1 \
./venv/bin/python -m pytest services/tests/eval --tb=short -ra

# Run only eval-marked cases from the agent-eval directory.
AIRUNNER_TEST_NO_GUI_LAUNCH=1 \
./venv/bin/python -m pytest services/tests/eval -m eval --tb=short -ra

# Run one suite.
AIRUNNER_TEST_NO_GUI_LAUNCH=1 \
./venv/bin/python -m pytest services/tests/eval/test_agent_tool_eval.py \
  --tb=short -ra

# Run non-forced tool-selection coverage.
AIRUNNER_TEST_NO_GUI_LAUNCH=1 \
./venv/bin/python -m pytest \
  services/tests/eval/test_agent_tool_selection_eval.py \
  --tb=short -ra

# Run the full attached-document eval suite.
AIRUNNER_TEST_NO_GUI_LAUNCH=1 \
./venv/bin/python -m pytest services/tests/eval/test_agent_document_eval.py \
  --tb=short -ra

# Run the legacy markdown document-retrieval slice.
AIRUNNER_TEST_NO_GUI_LAUNCH=1 \
./venv/bin/python -m pytest services/tests/eval/test_agent_document_eval.py \
  -k 'codename or numeric' --tb=short -ra

# Run the tri-format Time Machine document-retrieval slice.
AIRUNNER_TEST_NO_GUI_LAUNCH=1 \
./venv/bin/python -m pytest services/tests/eval/test_agent_document_eval.py \
  -k time_machine_results --tb=short -ra

# Run only GPT-OSS-tagged cases across the eval directory.
AIRUNNER_TEST_NO_GUI_LAUNCH=1 \
./venv/bin/python -m pytest services/tests/eval \
  -k gpt-oss-20b --tb=short -ra

# Run judged response evals with Groq as the external judge.
# NOTE: GROQ_API_KEY must be exported for Python/pytest to see it.
source ~/.bashrc
export GROQ_API_KEY="$GROQ_API_KEY"
AIRUNNER_TEST_JUDGE_SERVICE=groq \
AIRUNNER_TEST_JUDGE_MODEL=llama-3.3-70b-versatile \
AIRUNNER_TEST_NO_GUI_LAUNCH=1 \
./venv/bin/python -m pytest \
  services/tests/eval/test_agent_response_eval.py \
  -k 'plain-chat' --tb=short -ra

Coverage Matrix

SuiteCurrent model coverageWhat it proves
test_agent_flow_eval.pyqwen3.5-9b, gpt-oss-20bBaseline chat path plus one forced argument-bearing tool path
test_agent_mood_eval.pyqwen3.5-9b, gpt-oss-20bMood updates from a persisted follow-up turn
test_agent_response_eval.pyqwen3.5-9b, gpt-oss-20bFinal response quality against short references using the service-owned judge helpers
test_agent_tool_eval.pyqwen3.5-9b, gpt-oss-20bDeterministic forced tool coverage for conversation/system/math/qa/file/search surfaces
test_agent_tool_selection_eval.pyqwen3.5-9b, gpt-oss-20bNon-forced routing quality for datetime, math, and search category selection
test_agent_document_eval.pyqwen3.5-9b, gpt-oss-20bAttached-document loading plus rag_search retrieval routed back into the workflow for the legacy markdown fixture and the vendored Time Machine EPUB/MOBI/PDF matrix

Current parity note: the five service-owned eval files now cover both qwen3.5-9b and gpt-oss-20b. The response-heavy GPT-OSS slices are still materially slower than the deterministic tool slices, but the coverage surface is now aligned.

What The Tests Do

Shared harness files:

  • services/tests/llm_functional_support.py starts a real headless daemon on an ephemeral port, waits for /health, and tears it down after the case finishes.
  • services/tests/eval/agent_eval_support.py builds stable request payloads, posts them to /llm/generate, strips hidden thinking from the visible reply, and provides helper assertions.

Per-case flow:

  1. started_daemon() boots a real service daemon with deterministic test env overrides.
  2. build_agent_request() constructs a stable request. For Qwen, it also prepends /no_think for short deterministic prompts.
  3. run_agent_eval_case() posts the request to /llm/generate.
  4. Assertions check one or more of these surfaces:
    • assistant-visible reply text,
    • tool names returned in the payload or daemon log,
    • daemon log side effects such as mood updates or retrieved document evidence,
    • LLM-as-judge scores for short judged response cases.

Tool Surfaces Covered Today

  • python_compute, sympy_compute, and numpy_compute cover forced deterministic math execution.
  • clear_chat_history and get_current_datetime cover deterministic conversation/system execution.
  • identify_answer_type and list_directory cover non-search QA/file tool execution.
  • scrape_website covers deterministic website extraction invocation.
  • test_agent_tool_selection_eval.py validates non-forced selection behavior for datetime, math, and search intents using payload, log, and message fallback parsing.
  • rag_search covers attached-document retrieval via request-scoped rag_files, including the vendored The Time Machine .epub, .mobi, and .pdf fixtures.
  • Mood behavior is verified through the service log path because the authoritative update happens inside services, not the GUI.

The evals intentionally prefer deterministic surfaces. No-argument or return-direct tools are currently more stable than wider free-form tool use. That is why the individual tool file stays narrow while the flow suites cover broader orchestration behavior.

For local GGUF models, some non-forced selection cases may complete tool selection but still return 504 Gateway Timeout on the final synthesis turn. Selection assertions therefore rely on observed tool calls in the payload and daemon log, with message-text fallback parsing for model formats that emit pseudo-calls.

Groq judged eval note: in this shell setup, GROQ_API_KEY can exist as a non-exported shell variable after .bashrc sourcing. Always export it before running Python/pytest or the judge provider will fail to resolve the key.

Strict Tool Inventory Matrix

Legend:

  • Forced: Covered in test_agent_tool_eval.py
  • Selection: Covered in test_agent_tool_selection_eval.py
  • Doc/RAG: Covered in test_agent_document_eval.py
  • Recommendation: keep, review, or remove-candidate
ToolCategoryForcedSelectionDoc/RAGRecommendation
clear_chat_historyconversationyesnonokeep
get_current_datetimesystemyesyesnokeep
python_computemathyesyesnokeep
sympy_computemathyesyesnokeep
numpy_computemathyesyesnokeep
identify_answer_typeqayesnonokeep
list_directoryfileyesnonokeep
scrape_websitesearchyesnonokeep
search_websearchnoyesnokeep
search_newssearchnoyesnokeep
rag_searchragnonoyeskeep
search_knowledge_base_documentssearchnononoreview
save_to_knowledge_baseragnononoreview
record_knowledgeknowledgenononoreview
recall_knowledgeknowledgenononoreview
read_knowledge_fileknowledgenononoreview
update_knowledgeknowledgenononoreview
delete_knowledgeknowledgenononoreview
list_knowledge_filesknowledgenononoreview
store_user_dataknowledgenononoreview
get_user_dataknowledgenononoreview
verify_answerqanononoreview
score_answer_confidenceqanononoreview
extract_answer_from_contextqanononoreview
generate_clarifying_questionsqanononoreview
rank_answer_candidatesqanononoreview
get_conversation_summaryconversationnononoreview
load_conversationconversationnononoreview
quit_applicationsystemnononoreview
toggle_ttssystemnononoreview
read_filefilenononoreview
write_filefilenononoreview
search_toolssystemnononoreview
list_available_toolssystemnononoremove-candidate
polya_reasoninganalysisnononoreview
chain_of_thoughtanalysisnononoremove-candidate
categorizeanalysisnononoreview
generate_direct_responsegenerationnononoreview
generate_descriptiongenerationnononoreview
update_moodmoodnononoreview
generate_imageimagenononoreview
set_image_dimensionsimagenononoreview
clear_canvasimagenononoreview
open_imageimagenononoreview
get_image_model_infoimagenononoreview
validate_urlresearchnononoreview
validate_contentresearchnononoreview
extract_age_from_textresearchnononoreview
get_current_date_contextresearchnononoreview
check_temporal_accuracyresearchnononoreview
validate_research_subjectresearchnononoreview
search_document_chunksresearchnononoreview
update_research_summaryresearchnononoreview
get_research_summaryresearchnononoreview
intelligent_crawlresearchnononoreview
improve_writingauthornononoreview
check_grammarauthornononoreview
find_synonymsauthornononoreview
analyze_writing_styleauthornononoreview
create_long_running_projectprojectnononoreview
initialize_project_featuresprojectnononoreview
get_project_statusprojectnononoreview
list_project_featuresprojectnononoreview
get_project_progress_logprojectnononoreview
list_long_running_projectsprojectnononoreview
add_project_featureprojectnononoreview
update_feature_statusprojectnononoreview
log_project_progressprojectnononoreview
get_next_feature_to_work_onprojectnononoreview

The document eval file intentionally validates the service-owned retrieval path more than open-ended document QA. The Time Machine slice forces rag_search, uses an exact-term Morlocks prompt across the three file formats, and asserts on daemon-log retrieval signals rather than brittle post-tool wording.

The legacy markdown slice uses the same deterministic shape. It forces rag_search, asks for the exact phrase Project Alpha or the exact term 73142, keeps the request-scoped answer budget short, and asserts status code, executed tool names, and daemon-log retrieval anchors instead of exact post-tool wording.

GPT-OSS Notes

Observed constraints from the current GPT-OSS eval bring-up:

  • gpt-oss-20b runs these evals with AIRUNNER_GGUF_N_CTX=4096 and AIRUNNER_GGUF_N_GPU_LAYERS=0 in the shared harness.
  • test_agent_document_eval.py also hides CUDA for GPT-OSS document runs with CUDA_VISIBLE_DEVICES=''; leaving CUDA visible can fail GGUF llama_context creation on this machine before the forced rag_search workflow initializes.
  • Forced GPT-OSS tool cases use the raw Harmony prompting path with a prefilled tool envelope. The current service parser now handles both a single bare JSON argument object and duplicated adjacent JSON objects, which showed up on clear_chat_history during parity bring-up.
  • Deterministic tool, mood, response, and attached-document slices now pass through the same daemon-backed files as Qwen.

Qwen Notes

Observed constraints from the current Qwen eval bring-up:

  • qwen3.5-9b runs these evals with AIRUNNER_GGUF_N_CTX=4096 and AIRUNNER_GGUF_N_GPU_LAYERS=10 in the shared harness.
  • test_agent_document_eval.py hides CUDA for all document runs with CUDA_VISIBLE_DEVICES=''. Qwen document runs also override AIRUNNER_GGUF_N_GPU_LAYERS=0 and AIRUNNER_LOCAL_FALLBACK_TIMEOUT_SECONDS=900 because the document/RAG slice can otherwise fail GGUF llama_context creation or time out before the bounded post-tool answer completes on this machine.
  • Exact stylistic assertions are brittle. Qwen can split visible words such as Cheerful, so punctuation or judged-quality checks are more stable than style-word matching.
  • Forced tool cases are most reliable on deterministic tool surfaces.
  • Attached-document retrieval through rag_search is stable, but the visible post-tool final answer is still less deterministic than the retrieved tool result. The document evals therefore assert on the service-owned retrieval signal in the daemon log rather than exact final wording.
  • The document eval request budget is kept short (max_new_tokens=64) so Qwen has enough room to emit the forced rag_search call while the follow-up answer turn stays bounded.
  • After a forced document retrieval succeeds, services now unbind tools for the follow-up answer turn so Qwen answers from the retrieved tool result instead of recursively re-invoking rag_search.

Recent local timings from validated slices:

SliceResult
test_agent_document_eval.py (full file)10 passed in 2180.13s (0:36:20)
test_agent_tool_eval.py (Qwen + GPT-OSS)4 passed in 170.49s
test_agent_tool_eval.py (Qwen-only)2 passed in 53.67s
test_agent_document_eval.py -k 'codename or numeric'4 passed, 6 deselected in 451.11s
test_agent_document_eval.py -k time_machine_results6 passed, 4 deselected in 455.94s
test_agent_flow_eval.py (shared flow slice)3 passed in 178.10s
test_agent_flow_eval.py -k forced_math_tool_flow (Qwen-only)1 passed in 52.25s
test_agent_response_eval.py -k personality-prompt (Qwen-only)1 passed, 1 deselected in 177.16s

Treat those numbers as current local observations, not as a fixed benchmark contract.