Add AI-assisted triage#3
Conversation
Also make the agent perma prompt part of the package
- make it part of the markdown when that is used - save to separate file if not used - print to stdout for ai-triage command
grayson-wolf
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
| - **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
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
are these intended to be commented out?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
I would say get this in, and then refine the prompt later
TheJJ
left a comment
There was a problem hiding this comment.
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 :)
| 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" |
There was a problem hiding this comment.
appended? ideally it would just be that file then ->. a complete triage report? :)
| ai_triage_p = sp.add_parser( | ||
| "ai-triage", | ||
| help="AI-triage one or more Launchpad bugs", | ||
| ) |
There was a problem hiding this comment.
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.
| ) | ||
|
|
||
|
|
||
| def _build_ai_provider(config: StarTriageConfig): |
| async def _ai_triage_results( | ||
| config: StarTriageConfig, | ||
| results: Sequence[tuple[str, TriageResult]], | ||
| provider, |
| 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) |
There was a problem hiding this comment.
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.
| auth is needed. The token is resolved with config-over-env precedence via | ||
| :meth:`AIConfig.resolve_token`. | ||
| """ | ||
| if ai_config.provider is AIProvider.copilot: |
There was a problem hiding this comment.
could go with a match statement here.
| 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: |
There was a problem hiding this comment.
could go with a match statement here.
| 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, |
There was a problem hiding this comment.
that should only be done in the container. people should assume running startriage on their hosts doesn't extract precious cat pictures.
| try: | ||
| target.write_text(content, encoding="utf-8") | ||
| return target | ||
| except OSError: |
There was a problem hiding this comment.
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.
| provider = "openrouter" | ||
| model = "anthropic/claude-3.5-sonnet" | ||
| openrouter_api_key = "or_secret" | ||
| """, |
There was a problem hiding this comment.
indent wrong (doesn't ruff format complain?)
This adds the whole AI-assisted functionality to startriage. It's full of abstractions and details but it works well.
--aifor usual triage second opinionai-triagefor single bugs