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
1 change: 1 addition & 0 deletions src/clio_agent/arc/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

try:
from lru import LRU # lru-dict package

HAS_LRU_DICT = True
except ImportError:
HAS_LRU_DICT = False
Expand Down
38 changes: 15 additions & 23 deletions src/clio_agent/arc/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,7 @@ def search(self, key: Tuple[str, float]) -> Optional[Any]:
return self._index.get(key)

def range_query(
self,
start_key: Tuple[str, float],
end_key: Tuple[str, float],
inclusive: bool = True
self, start_key: Tuple[str, float], end_key: Tuple[str, float], inclusive: bool = True
) -> List[Any]:
"""
Retrieve all values within key range [start_key, end_key].
Expand Down Expand Up @@ -103,13 +100,13 @@ def range_query(
... )
"""
# irange returns keys, so we need to extract values
return [self._index[k] for k in self._index.irange(start_key, end_key, inclusive=(True, inclusive))]
return [
self._index[k]
for k in self._index.irange(start_key, end_key, inclusive=(True, inclusive))
]

def range_query_keys(
self,
start_key: Tuple[str, float],
end_key: Tuple[str, float],
inclusive: bool = True
self, start_key: Tuple[str, float], end_key: Tuple[str, float], inclusive: bool = True
) -> List[Tuple[str, float]]:
"""
Retrieve all keys within range [start_key, end_key].
Expand All @@ -135,10 +132,7 @@ def range_query_keys(
return list(self._index.irange(start_key, end_key, inclusive=(True, inclusive)))

def range_query_items(
self,
start_key: Tuple[str, float],
end_key: Tuple[str, float],
inclusive: bool = True
self, start_key: Tuple[str, float], end_key: Tuple[str, float], inclusive: bool = True
) -> List[Tuple[Tuple[str, float], Any]]:
"""
Retrieve all (key, value) pairs within range.
Expand All @@ -162,7 +156,10 @@ def range_query_items(
... print(f"{key}: {value}")
"""
# Return (key, value) tuples for all keys in range
return [(k, self._index[k]) for k in self._index.irange(start_key, end_key, inclusive=(True, inclusive))]
return [
(k, self._index[k])
for k in self._index.irange(start_key, end_key, inclusive=(True, inclusive))
]

def delete(self, key: Tuple[str, float]) -> bool:
"""
Expand Down Expand Up @@ -249,10 +246,7 @@ def get_session_range(self, session_id: str) -> List[Any]:
Examples:
>>> conversations = index.get_session_range("session_1")
"""
return self.range_query(
(session_id, 0.0),
(session_id, float('inf'))
)
return self.range_query((session_id, 0.0), (session_id, float("inf")))

def get_latest_in_session(self, session_id: str, n: int = 1) -> List[Any]:
"""
Expand All @@ -279,11 +273,9 @@ def get_latest_in_session(self, session_id: str, n: int = 1) -> List[Any]:
>>> recent = index.get_latest_in_session("session_1", n=5)
"""
# Get keys in reverse order (most recent first)
all_keys = list(self._index.irange(
(session_id, 0.0),
(session_id, float('inf')),
reverse=True
))
all_keys = list(
self._index.irange((session_id, 0.0), (session_id, float("inf")), reverse=True)
)
# Take first n keys (the n most recent)
latest_keys = all_keys[:n]
# Return values in chronological order (oldest to newest)
Expand Down
4 changes: 1 addition & 3 deletions src/clio_agent/arc/lsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,7 @@ def range_scan(self, start_ts: float, end_ts: float) -> List[Dict[str, Any]]:
if end_ts < sstable.min_key or start_ts > sstable.max_key:
continue

sstable_results = self._range_scan_sstable(
sstable, start_ts, end_ts
)
sstable_results = self._range_scan_sstable(sstable, start_ts, end_ts)
for ts, metric in sstable_results.items():
# MemTable has priority (newer data)
if ts not in results:
Expand Down
8 changes: 3 additions & 5 deletions src/clio_agent/arc/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ def __init__(

# Tier migration policy (days)
self.tier_policy = tier_policy or {
"hot_to_warm": 1, # 1 day in hot tier before eviction
"warm_to_cold": 7, # 1 week in warm tier
"hot_to_warm": 1, # 1 day in hot tier before eviction
"warm_to_cold": 7, # 1 week in warm tier
"cold_to_archive": 30, # 1 month in cold tier
}

Expand Down Expand Up @@ -476,9 +476,7 @@ def _maybe_migrate_tiers(self) -> None:

# Parse timestamp
try:
last_accessed = datetime.fromisoformat(
last_accessed_str.replace("Z", "+00:00")
)
last_accessed = datetime.fromisoformat(last_accessed_str.replace("Z", "+00:00"))
except Exception:
continue

Expand Down
3 changes: 2 additions & 1 deletion src/clio_agent/conversation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class ConversationManagerSignature(dspy.Signature):
- key_topics: Main topics discussed
- context_for_response: Relevant context
"""

history: dspy.History = dspy.InputField(desc="Conversation history")
current_question: str = dspy.InputField(desc="Current question")
summary: str = dspy.OutputField(desc="Summary of context")
Expand All @@ -63,7 +64,7 @@ def add_message(self, role: str, content: str):
self.history_buffer.append({"role": role, "content": content})
if len(self.history_buffer) > self.max_history_length:
# Keep only recent messages
self.history_buffer = self.history_buffer[-self.max_history_length:]
self.history_buffer = self.history_buffer[-self.max_history_length :]

def get_history(self) -> dspy.History:
"""Get current history as dspy.History."""
Expand Down
1 change: 1 addition & 0 deletions src/clio_agent/experts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
# EXPERT REGISTRY
# ============================================================================


def get_all_experts() -> Dict[str, dspy.Module]:
"""Get all available expert instances.

Expand Down
4 changes: 1 addition & 3 deletions src/clio_agent/experts/ndp_expert.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ def __init__(self, tool_executor: ToolExecutor | None = None) -> None:
self._owns_executor = tool_executor is None
self._tool_executor = tool_executor or create_sync_tool_executor(gateway)
self._tools = [
tool
for tool in self._tool_executor.to_dspy_tools()
if tool.name.startswith("ndp_")
tool for tool in self._tool_executor.to_dspy_tools() if tool.name.startswith("ndp_")
]

def forward(self, question: str, file_context: str = "") -> dspy.Prediction:
Expand Down
7 changes: 2 additions & 5 deletions src/clio_agent/experts/sac_format_expert.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@ def __init__(self, tool_executor: ToolExecutor | None = None) -> None:
self._owns_executor = tool_executor is None
self._tool_executor = tool_executor or create_sync_tool_executor(gateway)
self._tools = [
tool
for tool in self._tool_executor.to_dspy_tools()
if tool.name.startswith("sac_")
tool for tool in self._tool_executor.to_dspy_tools() if tool.name.startswith("sac_")
]

def forward(self, question: str, file_context: str = "") -> dspy.Prediction:
Expand Down Expand Up @@ -229,8 +227,7 @@ def plot_traces(self, filepath: str) -> ExpertResult:
if isinstance(result, dict) and result.get("error"):
return ExpertResult(
analysis=(
"Could not create SAC waveform plot: "
f"{format_tool_error(result['error'])}"
f"Could not create SAC waveform plot: {format_tool_error(result['error'])}"
),
recommendations="Verify the staged file and SAC plotting tool contract.",
source="deterministic",
Expand Down
8 changes: 2 additions & 6 deletions src/clio_agent/gact/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ class EventBus:
instances which serialize their own operations.
"""

def __init__(
self, *, queue_capacity: int = 256, history_per_session: int = 256
) -> None:
def __init__(self, *, queue_capacity: int = 256, history_per_session: int = 256) -> None:
self._capacity = queue_capacity
self._history_cap = history_per_session
# session_id -> list of subscriber queues
Expand Down Expand Up @@ -126,9 +124,7 @@ def publish(self, event: Event) -> None:
except asyncio.QueueFull:
pass

async def subscribe(
self, session_id: str, *, last_event_id: int = 0
) -> AsyncIterator[Event]:
async def subscribe(self, session_id: str, *, last_event_id: int = 0) -> AsyncIterator[Event]:
"""Yield events for ``session_id`` until the consumer drops.

``last_event_id`` is the highest event id the client already
Expand Down
12 changes: 3 additions & 9 deletions src/clio_agent/gact/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,7 @@ def _flush(self) -> None:
)
tmp.replace(self._path)

def add(
self, *, session_id: str, cron: str, question: str
) -> Schedule:
def add(self, *, session_id: str, cron: str, question: str) -> Schedule:
sid = "sched_" + uuid.uuid4().hex[:12]
sch = Schedule(
id=sid,
Expand All @@ -140,9 +138,7 @@ def get(self, sid: str) -> Optional[Schedule]:
with self._lock:
return self._schedules.get(sid)

def list(
self, *, session_id: Optional[str] = None
) -> list[Schedule]:
def list(self, *, session_id: Optional[str] = None) -> list[Schedule]:
with self._lock:
rows = list(self._schedules.values())
if session_id is not None:
Expand Down Expand Up @@ -174,9 +170,7 @@ def due_now(self, when: datetime) -> Iterable[Schedule]:
for sch in rows:
if not sch.enabled:
continue
if sch.last_fired_at and sch.last_fired_at.startswith(
when_minute
):
if sch.last_fired_at and sch.last_fired_at.startswith(when_minute):
continue
if cron_matches(sch.cron, when):
yield sch
5 changes: 4 additions & 1 deletion src/clio_agent/gact/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,10 @@ def update(
if edit_mode is not None and edit_mode in {"diff", "whole", "patch"}:
sess.edit_mode = edit_mode
if routing_mode is not None and routing_mode in {
"auto", "chat", "experts", "reasoning_only",
"auto",
"chat",
"experts",
"reasoning_only",
}:
sess.routing_mode = routing_mode
if model is not None:
Expand Down
37 changes: 23 additions & 14 deletions src/clio_agent/gact/user_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,20 +67,29 @@ def _load(self) -> None:
return
for row in data.get("agents", []):
try:
self._agents[row["id"]] = UserAgent(**{
k: row[k]
for k in (
"id", "title", "description", "source",
"system_prompt", "default_provider", "default_model",
"tier", "specialization",
)
if k in row
} | {
"parameters": dict(row.get("parameters", {})),
"keywords": list(row.get("keywords", [])),
"tools": list(row.get("tools", [])),
"metadata": dict(row.get("metadata", {})),
})
self._agents[row["id"]] = UserAgent(
**{
k: row[k]
for k in (
"id",
"title",
"description",
"source",
"system_prompt",
"default_provider",
"default_model",
"tier",
"specialization",
)
if k in row
}
| {
"parameters": dict(row.get("parameters", {})),
"keywords": list(row.get("keywords", [])),
"tools": list(row.get("tools", [])),
"metadata": dict(row.get("metadata", {})),
}
)
except Exception:
continue

Expand Down
10 changes: 9 additions & 1 deletion src/clio_agent/optimizer/instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def instrumented_forward(arc_memory: Any, agent_id: str) -> Callable:
... def forward(self, question, file_context=""):
... return dspy.Prediction(analysis="...", recommendations="...")
"""

def decorator(forward_fn: Callable) -> Callable:
@functools.wraps(forward_fn)
def wrapper(*args, **kwargs):
Expand Down Expand Up @@ -86,6 +87,7 @@ def wrapper(*args, **kwargs):
arc_memory.store_invocation(invocation)

return wrapper

return decorator


Expand Down Expand Up @@ -147,7 +149,13 @@ def _extract_output(result: Any) -> Dict[str, Any]:
pass
else:
# Fallback: try common expert output fields
for field in ("analysis", "recommendations", "visualization_description", "file_path", "answer"):
for field in (
"analysis",
"recommendations",
"visualization_description",
"file_path",
"answer",
):
val = getattr(result, field, None)
if val is not None:
output_data[field] = _to_safe_text(val)[:500]
Expand Down
3 changes: 1 addition & 2 deletions src/clio_agent/optimizer/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ def run(
"""
if len(trainset) < 5:
raise ValueError(
f"Need at least 5 training examples for 20/80 split. "
f"Got {len(trainset)}."
f"Need at least 5 training examples for 20/80 split. Got {len(trainset)}."
)

if metric_fn is None:
Expand Down
33 changes: 17 additions & 16 deletions src/clio_agent/optimizer/trainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@
from clio_agent.arc.schema import Invocation, decode_invocation

# Error keywords that indicate problematic output
_ERROR_KEYWORDS = frozenset([
"error:", "error,", "traceback", "exception:", "failed to",
"could not", "unable to", "runtime error", "type error",
])
_ERROR_KEYWORDS = frozenset(
[
"error:",
"error,",
"traceback",
"exception:",
"failed to",
"could not",
"unable to",
"runtime error",
"type error",
]
)


class TrainingSetGenerator:
Expand All @@ -46,9 +55,7 @@ def __init__(self, arc_memory: Any) -> None:
"""
self._arc = arc_memory

def generate(
self, agent_id: str, min_examples: int = 30
) -> list[dspy.Example]:
def generate(self, agent_id: str, min_examples: int = 30) -> list[dspy.Example]:
"""Generate training set from ARC invocations for a specific expert.

Calls arc_memory.get_invocations_by_agent with status="success",
Expand All @@ -73,9 +80,7 @@ def generate(
>>> assert len(examples) >= 30
>>> assert "question" in examples[0].inputs()
"""
invocations = self._arc.get_invocations_by_agent(
agent_id, status="success"
)
invocations = self._arc.get_invocations_by_agent(agent_id, status="success")

if len(invocations) < min_examples:
raise ValueError(
Expand Down Expand Up @@ -127,9 +132,7 @@ def get_available_counts(self) -> dict[str, int]:
return counts

@staticmethod
def _invocation_to_example(
inv: Invocation, agent_id: str
) -> dspy.Example | None:
def _invocation_to_example(inv: Invocation, agent_id: str) -> dspy.Example | None:
"""Convert a single Invocation to a dspy.Example.

Maps invocation input/output fields to expert signature fields.
Expand Down Expand Up @@ -182,9 +185,7 @@ def _invocation_to_example(
return None


def clio_expert_metric(
example: dspy.Example, pred: Any, trace: Any = None
) -> float | bool:
def clio_expert_metric(example: dspy.Example, pred: Any, trace: Any = None) -> float | bool:
"""Multi-signal metric for CLIO expert optimization.

Scores expert outputs on three weighted signals:
Expand Down
Loading
Loading