Skip to content

Add AI-assisted triage#3

Open
renanrodrigo wants to merge 11 commits into
ubuntu:mainfrom
renanrodrigo:ai-triage
Open

Add AI-assisted triage#3
renanrodrigo wants to merge 11 commits into
ubuntu:mainfrom
renanrodrigo:ai-triage

Conversation

@renanrodrigo

Copy link
Copy Markdown

This adds the whole AI-assisted functionality to startriage. It's full of abstractions and details but it works well.

  • Vibe coded
  • --ai for usual triage second opinion
  • ai-triage for single bugs

@grayson-wolf grayson-wolf left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good, to me, just a few suggestions / nit-questions

- **Affected package(s):** source package name(s)
- **Affected version(s):** package version and Ubuntu release(s)
- **Symptoms:** what goes wrong (error messages, crashes, incorrect output)
- **Reproduction steps:** how to trigger the bug (if known)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **Reproduction steps:** how to trigger the bug (if known)
- **Reproduction steps:** how to trigger the bug (if known). If feasible, these steps should work from a clean LXD container or VM.

I think this should be added because sometimes given reprosteps make a ton of assumptions

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say get this in, and then refine the prompt later

#model = "claude-opus-4.8" # copilot default; set your own for openrouter
#github_token = "github_pat_..." # or COPILOT_GITHUB_TOKEN / GH_TOKEN env
#openrouter_api_key = "..." # or OPENROUTER_API_KEY env
#openrouter_base_url = "https://openrouter.ai/api/v1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these intended to be commented out?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep only examples, defaults in the code.

2. **No hallucinated fixes.** If you cannot produce a fix with confidence that it is correct, set `proposed_fix.kind` to `none`. Do not invent plausible-looking patches.
3. **No patch application.** Do not generate or apply quilt patches. Do not modify any package source tree as a deliverable. Proposed fixes are returned as a unified diff in the `proposed_fix` field only.
4. **Read-only external access.** Do not post comments on bugs, change bug statuses, subscribe teams, or modify any external system. Your output is recommendations only; a human engineer will act on them.
5. **No speculation on internal architecture.** If you don't have enough information about a package's internals, say so rather than guessing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure on the best way to word this as a constraint (or if maybe this should be elsewhere) but an explicit instruction to verify upstream commits actually exist is probably a good idea (the tool has given me completely bogus commits that 404d on github before)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say get this in, and then refine the prompt later

@TheJJ TheJJ left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coming together pretty nicely. the overall architecture is good, i'd say.

one thing that worries me is the implicit assumption this is run in a secure container now:
how about a text-only analysis without funny containers as the first agent, and when it decides we wanna run shell stuff, it spins up a subagent in an lxd container (using the same mechanism we use for git ubuntu deltarebase resolving - we can hopefully create a shared ai lxd containering library so we don't have to duplicate/reinvent stuff)

otherwise many nits and naming issues, you'll see. great work, overall :)

Comment thread startriage/cli.py
action="store_true",
help=(
"Also run AI triage on every bug found. With --markdown the AI report is "
"appended to that file; otherwise it is written to autotriage-YYYY-MM-DD.md"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appended? ideally it would just be that file then ->. a complete triage report? :)

Comment thread startriage/cli.py
ai_triage_p = sp.add_parser(
"ai-triage",
help="AI-triage one or more Launchpad bugs",
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think a better name would be an option analyze --ai $what (without --ai it could just show the metadata of these bugs). then we have the option to extend the analysis into fixing with --fix, etc someday.

Comment thread startriage/cli.py
)


def _build_ai_provider(config: StarTriageConfig):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return should be typed properly

Comment thread startriage/cli.py
async def _ai_triage_results(
config: StarTriageConfig,
results: Sequence[tuple[str, TriageResult]],
provider,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type?

Comment thread startriage/cli.py
Comment on lines +411 to +476
def _build_ai_provider(config: StarTriageConfig):
"""Build the AI provider, printing a friendly hint and returning None on misconfig."""
from .ai import build_provider

try:
return build_provider(config.ai)
except AIConfigError as exc:
print(f"error: {exc}", file=sys.stderr)
return None


async def _ai_triage_results(
config: StarTriageConfig,
results: Sequence[tuple[str, TriageResult]],
provider,
markdown_path: Path | None,
) -> None:
"""Run the AI agent over the Launchpad tasks gathered by a normal triage run."""
from .ai import payloads_from_tasks, run_agent_on_payloads
from .sources.launchpad.triage import LaunchpadTriage

tasks: list = []
for _, result in results:
if isinstance(result, LaunchpadTriage):
tasks = list(result.tasks.tasks)
break

payloads = await asyncio.to_thread(payloads_from_tasks, tasks)
report = await run_agent_on_payloads(config, payloads, provider=provider)
if report is None:
return
_emit_ai_report(report, markdown_path)


def _emit_ai_report(report: str, markdown_path: Path | None) -> None:
"""Persist an AI ``report`` for a ``triage --ai`` run.

With ``--markdown`` the report is appended (behind a notice) to that file,
mirroring how the normal triage markdown is produced. Otherwise it is written
to a dated ``autotriage-<date>.md`` file and the path is shown on stdout.
"""
from .ai import append_report, write_report

if markdown_path is not None:
append_report(markdown_path, report)
print(f"AI triage appended to {markdown_path}")
else:
path = write_report(report)
print(f"AI triage report written to {path}")


async def _run_ai_triage(args: argparse.Namespace, config: StarTriageConfig) -> None:
from .ai import gather_user_bug_payloads, run_agent_on_payloads

provider = _build_ai_provider(config)
if provider is None:
return

payloads = await asyncio.to_thread(gather_user_bug_payloads, args.bug)
if not payloads:
print("No valid bugs to triage.", file=sys.stderr)
return

report = await run_agent_on_payloads(config, payloads, provider=provider)
if report is not None:
print(report)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a lot of code for running, this should rather be moved to the ai/ subdir, since the cli.py is a thin entrypoint for the real logic of available tasks. the idea is that you can run various actions directly over the python api, and cli.py just contains the wrappers that make it cli-runnable.

Comment thread startriage/ai/provider.py
auth is needed. The token is resolved with config-over-env precedence via
:meth:`AIConfig.resolve_token`.
"""
if ai_config.provider is AIProvider.copilot:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could go with a match statement here.

Comment thread startriage/ai/provider.py
Only OpenRouter (BYOK) contributes here, as an OpenAI-compatible ``provider``
block; the Copilot provider authenticates at the client level instead.
"""
if ai_config.provider is AIProvider.openrouter:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could go with a match statement here.

Comment thread startriage/ai/provider.py
logger.debug("Starting Copilot session (model=%s)", self.model)
async with CopilotClient(**build_client_kwargs(self._ai_config)) as client:
async with await client.create_session(
on_permission_request=PermissionHandler.approve_all,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that should only be done in the container. people should assume running startriage on their hosts doesn't extract precious cat pictures.

Comment thread startriage/ai/render.py
try:
target.write_text(content, encoding="utf-8")
return target
except OSError:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's bad error handling. we should determine a location before even starting analysis, so we're pretty sure writing will work before starting work.

Comment thread tests/test_config.py
provider = "openrouter"
model = "anthropic/claude-3.5-sonnet"
openrouter_api_key = "or_secret"
""",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indent wrong (doesn't ruff format complain?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants