feat(live-conversation): add search_repo tool over karti-ai/podman

Voice agent can now search the repo (code, symbols, files) via ripgrep over
the live local checkout of main. GitHub code-search index is empty for this
repo, so back it with the always-current clone instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kartikeya
2026-06-28 02:05:28 -07:00
parent 277cee8a7e
commit 87c271995c
+83
View File
@@ -2,6 +2,7 @@ import asyncio
import json import json
import logging import logging
import os import os
import subprocess
import time import time
from typing import Any from typing import Any
from urllib import error, request from urllib import error, request
@@ -22,6 +23,29 @@ MODEL = os.getenv("GEMINI_CONVERSATION_MODEL", "gemini-3.1-flash-live-preview")
VOICE = os.getenv("GEMINI_CONVERSATION_VOICE", "Aoede") VOICE = os.getenv("GEMINI_CONVERSATION_VOICE", "Aoede")
BACKEND_URL = os.getenv("PODMAN_BACKEND_URL", "http://127.0.0.1:8787").rstrip("/") BACKEND_URL = os.getenv("PODMAN_BACKEND_URL", "http://127.0.0.1:8787").rstrip("/")
INTERNAL_AGENT_TOKEN = os.getenv("INTERNAL_AGENT_TOKEN", "") INTERNAL_AGENT_TOKEN = os.getenv("INTERNAL_AGENT_TOKEN", "")
REPO_SLUG = os.getenv("PODMAN_REPO_SLUG", "karti-ai/podman")
def _resolve_repo_root() -> str:
override = os.getenv("PODMAN_REPO_ROOT")
if override:
return override
here = os.path.dirname(os.path.abspath(__file__))
try:
out = subprocess.run(
["git", "-C", here, "rev-parse", "--show-toplevel"],
capture_output=True,
text=True,
timeout=5,
)
if out.returncode == 0 and out.stdout.strip():
return out.stdout.strip()
except Exception:
pass
return os.path.abspath(os.path.join(here, "..", ".."))
REPO_ROOT = _resolve_repo_root()
INSTRUCTIONS = """You are PodMan, a concise real-time engineering teammate. INSTRUCTIONS = """You are PodMan, a concise real-time engineering teammate.
You are in a private 1:1 voice conversation with one developer. You are in a private 1:1 voice conversation with one developer.
@@ -29,6 +53,7 @@ You are in a private 1:1 voice conversation with one developer.
Use PodMan tools before making claims about current work, git state, collisions, blockers, Use PodMan tools before making claims about current work, git state, collisions, blockers,
team memory, or recent decisions. Keep spoken answers short. Prefer one useful next step. team memory, or recent decisions. Keep spoken answers short. Prefer one useful next step.
If a critical collision event arrives, stop the current turn and state the alert immediately. If a critical collision event arrives, stop the current turn and state the alert immediately.
To find code, files, symbols, or how something is implemented in the repository, call search_repo.
For complex repository, terminal, GitHub, MongoDB, build, install, deploy, or multi-step tasks, For complex repository, terminal, GitHub, MongoDB, build, install, deploy, or multi-step tasks,
call delegate_to_hermes. Do not run those actions directly. If the user says stop, wait, cancel, call delegate_to_hermes. Do not run those actions directly. If the user says stop, wait, cancel,
or change of plans while Hermes is running, call abort_active_hermes_job immediately. or change of plans while Hermes is running, call abort_active_hermes_job immediately.
@@ -129,6 +154,64 @@ class PodManLiveAgent(Agent):
snippets.append(haystack[max(0, idx - 400) : idx + 1200]) snippets.append(haystack[max(0, idx - 400) : idx + 1200])
return "\n---\n".join(snippets)[:8000] or haystack[:6000] return "\n---\n".join(snippets)[:8000] or haystack[:6000]
@function_tool()
async def search_repo(self, context: RunContext, query: str, max_results: int = 12) -> str:
"""Search the team's code repository (github.com/karti-ai/podman) for code, symbols,
filenames, config, or any text. Use this to find where something is implemented or which
files mention a term before answering questions about the codebase. Searches the live
local checkout of the main branch, so results are always current.
"""
cleaned = " ".join(query.split()).strip()
if not cleaned:
return "Provide a non-empty search query."
limit = max(1, min(int(max_results or 12), 40))
cmd = [
"rg",
"--line-number",
"--no-heading",
"--color",
"never",
"--smart-case",
"--max-count",
"3",
"--max-columns",
"240",
"-g",
"!*.lock",
"-g",
"!pnpm-lock.yaml",
"-g",
"!uv.lock",
"-g",
"!*.min.*",
"--",
cleaned,
REPO_ROOT,
]
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=15)
except asyncio.TimeoutError:
return "Repo search timed out. Try a more specific query."
except FileNotFoundError:
return "Repo search is unavailable on this host (ripgrep is not installed)."
if proc.returncode not in (0, 1): # rg: 0=match, 1=no match, 2=error
return f"Repo search failed: {stderr.decode('utf-8', 'replace')[:300]}"
prefix = REPO_ROOT + os.sep
lines: list[str] = []
for line in stdout.decode("utf-8", "replace").splitlines():
lines.append(line[len(prefix) :] if line.startswith(prefix) else line)
if len(lines) >= limit:
break
if not lines:
return f'No matches for "{cleaned}" in {REPO_SLUG}.'
body = "\n".join(lines)
return f'Matches for "{cleaned}" in {REPO_SLUG} (path:line):\n{body}'[:7000]
@function_tool() @function_tool()
async def delegate_to_hermes( async def delegate_to_hermes(
self, self,