Co-authored-by: Google Gemini <gemini@hermes.local> Co-committed-by: Google Gemini <gemini@hermes.local>
This commit was merged in pull request #1140.
This commit is contained in:
@@ -96,7 +96,7 @@ asyncio_default_fixture_loop_scope = "function"
|
|||||||
timeout = 30
|
timeout = 30
|
||||||
timeout_method = "signal"
|
timeout_method = "signal"
|
||||||
timeout_func_only = false
|
timeout_func_only = false
|
||||||
addopts = "-v --tb=short --strict-markers --disable-warnings --durations=10"
|
addopts = "-v --tb=short --strict-markers --disable-warnings --durations=10 --cov-fail-under=60"
|
||||||
markers = [
|
markers = [
|
||||||
"unit: Unit tests (fast, no I/O)",
|
"unit: Unit tests (fast, no I/O)",
|
||||||
"integration: Integration tests (may use SQLite)",
|
"integration: Integration tests (may use SQLite)",
|
||||||
|
|||||||
@@ -384,11 +384,12 @@ def _startup_background_tasks() -> list[asyncio.Task]:
|
|||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
from timmy.paperclip import start_paperclip_poller
|
from timmy.paperclip import start_paperclip_poller
|
||||||
|
|
||||||
bg_tasks.append(asyncio.create_task(start_paperclip_poller()))
|
bg_tasks.append(asyncio.create_task(start_paperclip_poller()))
|
||||||
logger.info("Paperclip poller started")
|
logger.info("Paperclip poller started")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.debug("Paperclip module not found, skipping poller")
|
logger.debug("Paperclip module not found, skipping poller")
|
||||||
|
|
||||||
return bg_tasks
|
return bg_tasks
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class Provider:
|
|||||||
type: str # ollama, openai, anthropic
|
type: str # ollama, openai, anthropic
|
||||||
enabled: bool
|
enabled: bool
|
||||||
priority: int
|
priority: int
|
||||||
tier: str | None = None # e.g., "local", "standard_cloud", "frontier"
|
tier: str | None = None # e.g., "local", "standard_cloud", "frontier"
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
api_key: str | None = None
|
api_key: str | None = None
|
||||||
base_url: str | None = None
|
base_url: str | None = None
|
||||||
@@ -573,7 +573,6 @@ class CascadeRouter:
|
|||||||
if not providers:
|
if not providers:
|
||||||
raise RuntimeError(f"No providers found for tier: {cascade_tier}")
|
raise RuntimeError(f"No providers found for tier: {cascade_tier}")
|
||||||
|
|
||||||
|
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
if not self._is_provider_available(provider):
|
if not self._is_provider_available(provider):
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ from dataclasses import dataclass
|
|||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
|
from timmy.research_tools import get_llm_client, google_web_search
|
||||||
from timmy.research_triage import triage_research_report
|
from timmy.research_triage import triage_research_report
|
||||||
from timmy.research_tools import google_web_search, get_llm_client
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -52,10 +52,7 @@ class PaperclipClient:
|
|||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
tasks = resp.json()
|
tasks = resp.json()
|
||||||
return [
|
return [PaperclipTask(id=t["id"], kind=t["kind"], context=t["context"]) for t in tasks]
|
||||||
PaperclipTask(id=t["id"], kind=t["kind"], context=t["context"])
|
|
||||||
for t in tasks
|
|
||||||
]
|
|
||||||
|
|
||||||
async def update_task_status(
|
async def update_task_status(
|
||||||
self, task_id: str, status: str, result: str | None = None
|
self, task_id: str, status: str, result: str | None = None
|
||||||
@@ -98,7 +95,7 @@ class ResearchOrchestrator:
|
|||||||
async def run_research_pipeline(self, issue_title: str) -> str:
|
async def run_research_pipeline(self, issue_title: str) -> str:
|
||||||
"""Run the research pipeline."""
|
"""Run the research pipeline."""
|
||||||
search_results = await google_web_search(issue_title)
|
search_results = await google_web_search(issue_title)
|
||||||
|
|
||||||
llm_client = get_llm_client()
|
llm_client = get_llm_client()
|
||||||
response = await llm_client.completion(
|
response = await llm_client.completion(
|
||||||
f"Summarize the following search results and generate a research report:\\n\\n{search_results}",
|
f"Summarize the following search results and generate a research report:\\n\\n{search_results}",
|
||||||
@@ -123,7 +120,9 @@ class ResearchOrchestrator:
|
|||||||
comment += "Created the following issues:\\n"
|
comment += "Created the following issues:\\n"
|
||||||
for result in triage_results:
|
for result in triage_results:
|
||||||
if result["gitea_issue"]:
|
if result["gitea_issue"]:
|
||||||
comment += f"- #{result['gitea_issue']['number']}: {result['action_item'].title}\\n"
|
comment += (
|
||||||
|
f"- #{result['gitea_issue']['number']}: {result['action_item'].title}\\n"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
comment += "No new issues were created.\\n"
|
comment += "No new issues were created.\\n"
|
||||||
|
|
||||||
@@ -172,4 +171,3 @@ async def start_paperclip_poller() -> None:
|
|||||||
if settings.paperclip_enabled:
|
if settings.paperclip_enabled:
|
||||||
poller = PaperclipPoller()
|
poller = PaperclipPoller()
|
||||||
asyncio.create_task(poller.poll())
|
asyncio.create_task(poller.poll())
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from config import settings
|
|
||||||
from serpapi import GoogleSearch
|
from serpapi import GoogleSearch
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -28,6 +27,7 @@ async def google_web_search(query: str) -> str:
|
|||||||
|
|
||||||
def get_llm_client() -> Any:
|
def get_llm_client() -> Any:
|
||||||
"""Get an LLM client."""
|
"""Get an LLM client."""
|
||||||
|
|
||||||
# This is a placeholder. In a real application, this would return
|
# This is a placeholder. In a real application, this would return
|
||||||
# a client for an LLM service like OpenAI, Anthropic, or a local
|
# a client for an LLM service like OpenAI, Anthropic, or a local
|
||||||
# model.
|
# model.
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import scripts.export_trajectories as et
|
import scripts.export_trajectories as et
|
||||||
|
|
||||||
|
|
||||||
# ── Fixtures ──────────────────────────────────────────────────────────────────
|
# ── Fixtures ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -22,10 +20,30 @@ def simple_session(tmp_path: Path) -> Path:
|
|||||||
logs_dir = tmp_path / "logs"
|
logs_dir = tmp_path / "logs"
|
||||||
logs_dir.mkdir()
|
logs_dir.mkdir()
|
||||||
entries = [
|
entries = [
|
||||||
{"type": "message", "role": "user", "content": "What time is it?", "timestamp": "2026-03-01T10:00:00"},
|
{
|
||||||
{"type": "message", "role": "timmy", "content": "It is 10:00 AM.", "timestamp": "2026-03-01T10:00:01"},
|
"type": "message",
|
||||||
{"type": "message", "role": "user", "content": "Thanks!", "timestamp": "2026-03-01T10:00:05"},
|
"role": "user",
|
||||||
{"type": "message", "role": "timmy", "content": "You're welcome!", "timestamp": "2026-03-01T10:00:06"},
|
"content": "What time is it?",
|
||||||
|
"timestamp": "2026-03-01T10:00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"role": "timmy",
|
||||||
|
"content": "It is 10:00 AM.",
|
||||||
|
"timestamp": "2026-03-01T10:00:01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"role": "user",
|
||||||
|
"content": "Thanks!",
|
||||||
|
"timestamp": "2026-03-01T10:00:05",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "message",
|
||||||
|
"role": "timmy",
|
||||||
|
"content": "You're welcome!",
|
||||||
|
"timestamp": "2026-03-01T10:00:06",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
session_file = logs_dir / "session_2026-03-01.jsonl"
|
session_file = logs_dir / "session_2026-03-01.jsonl"
|
||||||
session_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")
|
session_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")
|
||||||
@@ -38,7 +56,12 @@ def tool_call_session(tmp_path: Path) -> Path:
|
|||||||
logs_dir = tmp_path / "logs"
|
logs_dir = tmp_path / "logs"
|
||||||
logs_dir.mkdir()
|
logs_dir.mkdir()
|
||||||
entries = [
|
entries = [
|
||||||
{"type": "message", "role": "user", "content": "Read CLAUDE.md", "timestamp": "2026-03-01T10:00:00"},
|
{
|
||||||
|
"type": "message",
|
||||||
|
"role": "user",
|
||||||
|
"content": "Read CLAUDE.md",
|
||||||
|
"timestamp": "2026-03-01T10:00:00",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "tool_call",
|
"type": "tool_call",
|
||||||
"tool": "read_file",
|
"tool": "read_file",
|
||||||
@@ -46,7 +69,12 @@ def tool_call_session(tmp_path: Path) -> Path:
|
|||||||
"result": "# CLAUDE.md content here",
|
"result": "# CLAUDE.md content here",
|
||||||
"timestamp": "2026-03-01T10:00:01",
|
"timestamp": "2026-03-01T10:00:01",
|
||||||
},
|
},
|
||||||
{"type": "message", "role": "timmy", "content": "Here is the content.", "timestamp": "2026-03-01T10:00:02"},
|
{
|
||||||
|
"type": "message",
|
||||||
|
"role": "timmy",
|
||||||
|
"content": "Here is the content.",
|
||||||
|
"timestamp": "2026-03-01T10:00:02",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
session_file = logs_dir / "session_2026-03-01.jsonl"
|
session_file = logs_dir / "session_2026-03-01.jsonl"
|
||||||
session_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")
|
session_file.write_text("\n".join(json.dumps(e) for e in entries) + "\n")
|
||||||
@@ -236,7 +264,7 @@ def test_export_training_data_writes_jsonl(simple_session: Path, tmp_path: Path)
|
|||||||
count = et.export_training_data(logs_dir=simple_session, output_path=output)
|
count = et.export_training_data(logs_dir=simple_session, output_path=output)
|
||||||
assert count == 2
|
assert count == 2
|
||||||
assert output.exists()
|
assert output.exists()
|
||||||
lines = [json.loads(l) for l in output.read_text().splitlines() if l.strip()]
|
lines = [json.loads(line) for line in output.read_text().splitlines() if line.strip()]
|
||||||
assert len(lines) == 2
|
assert len(lines) == 2
|
||||||
for line in lines:
|
for line in lines:
|
||||||
assert "messages" in line
|
assert "messages" in line
|
||||||
@@ -270,16 +298,22 @@ def test_export_training_data_returns_zero_for_empty_logs(tmp_path: Path) -> Non
|
|||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_cli_missing_logs_dir(tmp_path: Path) -> None:
|
def test_cli_missing_logs_dir(tmp_path: Path) -> None:
|
||||||
rc = et.main(["--logs-dir", str(tmp_path / "nonexistent"), "--output", str(tmp_path / "out.jsonl")])
|
rc = et.main(
|
||||||
|
["--logs-dir", str(tmp_path / "nonexistent"), "--output", str(tmp_path / "out.jsonl")]
|
||||||
|
)
|
||||||
assert rc == 1
|
assert rc == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_cli_exports_and_returns_zero(simple_session: Path, tmp_path: Path) -> None:
|
def test_cli_exports_and_returns_zero(simple_session: Path, tmp_path: Path) -> None:
|
||||||
output = tmp_path / "out.jsonl"
|
output = tmp_path / "out.jsonl"
|
||||||
rc = et.main([
|
rc = et.main(
|
||||||
"--logs-dir", str(simple_session),
|
[
|
||||||
"--output", str(output),
|
"--logs-dir",
|
||||||
])
|
str(simple_session),
|
||||||
|
"--output",
|
||||||
|
str(output),
|
||||||
|
]
|
||||||
|
)
|
||||||
assert rc == 0
|
assert rc == 0
|
||||||
assert output.exists()
|
assert output.exists()
|
||||||
|
|||||||
@@ -9,19 +9,15 @@ Refs: #1105
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import tempfile
|
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from timmy_automations.retrain.quality_filter import QualityFilter, TrajectoryQuality
|
from timmy_automations.retrain.quality_filter import QualityFilter, TrajectoryQuality
|
||||||
from timmy_automations.retrain.retrain import RetrainOrchestrator
|
from timmy_automations.retrain.retrain import RetrainOrchestrator
|
||||||
from timmy_automations.retrain.training_dataset import TrainingDataset
|
from timmy_automations.retrain.training_dataset import TrainingDataset
|
||||||
from timmy_automations.retrain.training_log import CycleMetrics, TrainingLog
|
from timmy_automations.retrain.training_log import CycleMetrics, TrainingLog
|
||||||
from timmy_automations.retrain.trajectory_exporter import Trajectory, TrajectoryExporter
|
from timmy_automations.retrain.trajectory_exporter import Trajectory, TrajectoryExporter
|
||||||
|
|
||||||
|
|
||||||
# ── Fixtures ─────────────────────────────────────────────────────────────────
|
# ── Fixtures ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -382,7 +378,7 @@ class TestTrainingDataset:
|
|||||||
ds = TrainingDataset(repo_root=tmp_path)
|
ds = TrainingDataset(repo_root=tmp_path)
|
||||||
ds.append([self._make_result()], "2026-W12")
|
ds.append([self._make_result()], "2026-W12")
|
||||||
with open(ds.dataset_path) as f:
|
with open(ds.dataset_path) as f:
|
||||||
lines = [l.strip() for l in f if l.strip()]
|
lines = [line.strip() for line in f if line.strip()]
|
||||||
assert len(lines) == 1
|
assert len(lines) == 1
|
||||||
record = json.loads(lines[0])
|
record = json.loads(lines[0])
|
||||||
assert "messages" in record
|
assert "messages" in record
|
||||||
|
|||||||
Reference in New Issue
Block a user