diff --git a/agents/autowebcompat-diagnosis/hackbot_agents/autowebcompat_diagnosis/result.py b/agents/autowebcompat-diagnosis/hackbot_agents/autowebcompat_diagnosis/result.py index b51faea206..b1116fe919 100644 --- a/agents/autowebcompat-diagnosis/hackbot_agents/autowebcompat_diagnosis/result.py +++ b/agents/autowebcompat-diagnosis/hackbot_agents/autowebcompat_diagnosis/result.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path -from typing import Generic, Literal, TypeVar +from typing import Annotated, Generic, Literal, TypeVar from claude_agent_sdk import McpServerConfig, create_sdk_mcp_server, tool from pydantic import ( @@ -31,42 +31,57 @@ def __init__(self, result_cls: type[ResultT]) -> None: class DiagnosisPlanResult(BaseModel): """What the later tasks need, gathered before any browser is installed.""" - firefox_channel: Literal["nightly"] | Literal["stable"] | Literal["esr"] = Field( - description=("The Firefox channel to diagnose on."), - ) - - channel_rationale: str = Field( - description=( - "One or two sentences on why you chose that channel, citing what " - "you based it on (the `autowebcompat-repro-channels` marker, the " - "report text, or the absence of both)." + firefox_channel: Annotated[ + Literal["nightly"] | Literal["stable"] | Literal["esr"], + Field( + description=("The Firefox channel to diagnose on."), ), - ) - - url: str = Field( - description="The URL of the page the issue was reported on.", - ) - - steps: str = Field( - description=( - "The steps to reproduce the issue, as a single numbered list (1., " - "2., 3., ... one step per line), taken from the report and written " - "so another agent could follow them with no extra context. Each " - "step must be self-contained: whenever a step involves an input the " - "report did not provide, state its exact origin. Always fill this " - "in, even when a reproduction script is attached — the script may " - "turn out not to work." + ] + + channel_rationale: Annotated[ + str, + Field( + description=( + "One or two sentences on why you chose that channel, citing what " + "you based it on (the `autowebcompat-repro-channels` marker, the " + "report text, or the absence of both)." + ), ), - ) + ] - script_path: Path | None = Field( - description=( - "The file path you downloaded the attached Puppeteer reproduction " - "script to, or null if the bug has no such attachment. Use the " - "exact path you were given to write to (do NOT paste the script " - "source)." + url: Annotated[ + str, + Field( + description="The URL of the page the issue was reported on.", ), - ) + ] + + steps: Annotated[ + str, + Field( + description=( + "The steps to reproduce the issue, as a single numbered list (1., " + "2., 3., ... one step per line), taken from the report and written " + "so another agent could follow them with no extra context. Each " + "step must be self-contained: whenever a step involves an input the " + "report did not provide, state its exact origin. Always fill this " + "in, even when a reproduction script is attached — the script may " + "turn out not to work." + ), + ), + ] + + script_path: Annotated[ + Path | None, + Field( + description=( + "The file path you downloaded the attached Puppeteer reproduction " + "script to, or null if the bug has no such attachment. Use the " + "exact path you were given to write to (do NOT paste the script " + "source)." + ), + ), + ] @field_validator("script_path", mode="after") @classmethod @@ -84,28 +99,33 @@ def validate_script_path(cls, path: Path | None) -> Path | None: class ReproScriptResult(BaseModel): """Verdict from the script task: can the issue still be reproduced?""" - reproduced: bool = Field( - description=( - "true if you confirmed the reported issue still reproduces in " - "Firefox but not in Chrome, whether via a Puppeteer script or by " - "driving the site with the DevTools tools. false if you could not " - "reproduce it." + reproduced: Annotated[ + bool, + Field( + description=( + "true if you confirmed the reported issue still reproduces in " + "Firefox but not in Chrome, whether via a Puppeteer script or by " + "driving the site with the DevTools tools. false if you could not " + "reproduce it." + ), ), - ) - - failure_reason: ( - Literal["not_reproducible"] - | Literal["not_firefox_specific"] - | Literal["blocked"] - | Literal["blocked_captcha"] - | Literal["blocked_geo"] - | Literal["login"] - | Literal["down"] - | Literal["headless"] - | Literal["other"] - | None - ) = Field( - description="""Null if the issue reproduced. Otherwise the category + ] + + failure_reason: Annotated[ + ( + Literal["not_reproducible"] + | Literal["not_firefox_specific"] + | Literal["blocked"] + | Literal["blocked_captcha"] + | Literal["blocked_geo"] + | Literal["login"] + | Literal["down"] + | Literal["headless"] + | Literal["other"] + | None + ), + Field( + description="""Null if the issue reproduced. Otherwise the category describing why it did not: * not_reproducible - all the steps ran, but the reported issue did not occur * not_firefox_specific - the reported behavior reproduces in both Firefox @@ -119,24 +139,31 @@ class ReproScriptResult(BaseModel): headless environment * other - some other reason (give details in the summary) """, - ) - - summary: str = Field( - description=( - "A concise account of what you did and what you observed in each " - "browser, including why reproduction failed if it did." ), - ) - - script_path: Path | None = Field( - description=( - "The file path of the Puppeteer script that demonstrates the " - "difference — Firefox exits 1 and Chrome exits 0. Use the exact " - "path you were given to write to (do NOT paste the script source). " - "Null if no script validated; that is acceptable and does not by " - "itself mean the issue failed to reproduce." + ] + + summary: Annotated[ + str, + Field( + description=( + "A concise account of what you did and what you observed in each " + "browser, including why reproduction failed if it did." + ), ), - ) + ] + + script_path: Annotated[ + Path | None, + Field( + description=( + "The file path of the Puppeteer script that demonstrates the " + "difference — Firefox exits 1 and Chrome exits 0. Use the exact " + "path you were given to write to (do NOT paste the script source). " + "Null if no script validated; that is acceptable and does not by " + "itself mean the issue failed to reproduce." + ), + ), + ] @field_validator("script_path", mode="after") @classmethod @@ -167,9 +194,11 @@ def validate_consistency(self) -> ReproScriptResult: class DiagnosisResult(BaseModel): """The agent's root-cause account of why Firefox differs from Chrome.""" - root_cause: str = Field( - description=( - """Your root-cause hypothesis for why the site behaves differently in + root_cause: Annotated[ + str, + Field( + description=( + """Your root-cause hypothesis for why the site behaves differently in Firefox: what the page does, which behavior it depends on, and why that produces the reported breakage in Firefox but not Chrome. Be specific about the mechanism (e.g. the API, CSS property, or @@ -178,27 +207,34 @@ class DiagnosisResult(BaseModel): then if possible provide links to the relevant parts of the specification document that define the behaviour. Skip these links if you don't know the right specification or section. Do not propose a fix.""" + ), ), - ) - - evidence: str = Field( - description=( - "The concrete observations supporting the hypothesis: console " - "errors, network requests, DOM or computed-style measurements, " - "feature-detection results, and what the reduced testcase showed in " - "each browser. Be brief, this will be read by a busy engineer." - "Cite what you actually observed, not what you expect." + ] + + evidence: Annotated[ + str, + Field( + description=( + "The concrete observations supporting the hypothesis: console " + "errors, network requests, DOM or computed-style measurements, " + "feature-detection results, and what the reduced testcase showed in " + "each browser. Be brief, this will be read by a busy engineer." + "Cite what you actually observed, not what you expect." + ), ), - ) - testcase_path: Path | None = Field( - description=( - "The file path of the reduced HTML testcase you wrote. Set this only " - "if you loaded it in both browsers and confirmed it shows the same " - "difference as the real site. Use the exact path you were given to " - "write to (do NOT paste the HTML source). Null if you could not " - "produce a reduced testcase that reproduces the difference." + ] + testcase_path: Annotated[ + Path | None, + Field( + description=( + "The file path of the reduced HTML testcase you wrote. Set this only " + "if you loaded it in both browsers and confirmed it shows the same " + "difference as the real site. Use the exact path you were given to " + "write to (do NOT paste the HTML source). Null if you could not " + "produce a reduced testcase that reproduces the difference." + ), ), - ) + ] @field_validator("testcase_path", mode="after") @classmethod diff --git a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py index f9d1142d3c..72729750ed 100644 --- a/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py +++ b/agents/autowebcompat-repro/hackbot_agents/autowebcompat_repro/result.py @@ -4,7 +4,7 @@ import imghdr from pathlib import Path -from typing import Generic, Literal, TypeVar +from typing import Annotated, Generic, Literal, TypeVar from claude_agent_sdk import McpServerConfig, create_sdk_mcp_server, tool from pydantic import ( @@ -30,25 +30,34 @@ def __init__(self, result_cls: type[ResultT]) -> None: class TestPlanResult(BaseModel): - is_webcompat: bool = Field( - description=("true if the input describes a webcompat issue, otherwise false."), - ) - - affects_platforms: list[ - Literal["ios"] | Literal["android"] | Literal["desktop"] - ] = Field(description="List of platforms which seem to be affected by the issue") - - affects_os: ( - None - | Literal["all"] - | list[Literal["windows"] | Literal["linux"] | Literal["macos"]] - ) = Field( - description="""List of desktop issues known to be affected. + is_webcompat: Annotated[ + bool, + Field( + description=( + "true if the input describes a webcompat issue, otherwise false." + ), + ), + ] + + affects_platforms: Annotated[ + list[Literal["ios"] | Literal["android"] | Literal["desktop"]], + Field(description="List of platforms which seem to be affected by the issue"), + ] + + affects_os: Annotated[ + ( + None + | Literal["all"] + | list[Literal["windows"] | Literal["linux"] | Literal["macos"]] + ), + Field( + description="""List of desktop issues known to be affected. - `null` if the issue does not affect desktop. - "all" if there is no strong evidence that the issue is OS specific" - Otherwise a list of OS names which are likely affected """ - ) + ), + ] affects_channels: list[Literal["nightly"] | Literal["stable"] | Literal["esr"]] = ( Field( @@ -62,38 +71,46 @@ class TestPlanResult(BaseModel): class ReproductionResult(BaseModel): - confirmed_by_script: bool = Field( - default=False, - description=( - "true if a Puppeteer script demonstrated the difference for this " - "Firefox build, false if you could not get one to pass or did not " - "run one." + confirmed_by_script: Annotated[ + bool, + Field( + default=False, + description=( + "true if a Puppeteer script demonstrated the difference for this " + "Firefox build, false if you could not get one to pass or did not " + "run one." + ), ), - ) + ] - reproduced: bool = Field( - description=( - "true if the reported issue reproduced in Firefox, otherwise false." + reproduced: Annotated[ + bool, + Field( + description=( + "true if the reported issue reproduced in Firefox, otherwise false." + ), ), - ) - - failure_reason: ( - Literal["not_reproducible"] - | Literal["not_web_platform"] - | Literal["not_firefox_specific"] - | Literal["unsupported_android"] - | Literal["unsupported_ios"] - | Literal["unsupported_desktop_os"] - | Literal["blocked"] - | Literal["blocked_captcha"] - | Literal["blocked_geo"] - | Literal["login"] - | Literal["down"] - | Literal["headless"] - | Literal["other"] - | None - ) = Field( - description="""If an issue was reproduced as a Firefox web-compat issue then `null`. + ] + + failure_reason: Annotated[ + ( + Literal["not_reproducible"] + | Literal["not_web_platform"] + | Literal["not_firefox_specific"] + | Literal["unsupported_android"] + | Literal["unsupported_ios"] + | Literal["unsupported_desktop_os"] + | Literal["blocked"] + | Literal["blocked_captcha"] + | Literal["blocked_geo"] + | Literal["login"] + | Literal["down"] + | Literal["headless"] + | Literal["other"] + | None + ), + Field( + description="""If an issue was reproduced as a Firefox web-compat issue then `null`. Otherwise, one of the following categories describing the reason for the failure: * not_reproducible - When it was possible to run all the steps to reproduce, but no issue was found * not_web_platform - When the issue is not related to the the web content itself. This covers reports that don't refer @@ -111,17 +128,21 @@ class ReproductionResult(BaseModel): * headless - When there is an evidence that the issue isn't reproducible due to the headless environment * other - When the issue could not be reproduced for some other reason (briefly state the reason in the summary) """ - ) + ), + ] - screenshot_path: Path | None = Field( - description=( - """The file path you saved a screenshot to via the `screenshot_page` + screenshot_path: Annotated[ + Path | None, + Field( + description=( + """The file path you saved a screenshot to via the `screenshot_page` `saveTo` parameter, showing the issue. Use the exact path you passed as `saveTo` (do NOT paste image data). This must only be set for issues where the breakage is visual in nature i.e. incorrect site layout rather than broken interaction. Otherwise it must be null.""" + ), ), - ) + ] @field_validator("screenshot_path", mode="after") @classmethod @@ -143,47 +164,59 @@ class BugReproductionResult(ReproductionResult): Chrome so it can cross-check the two browsers in a single context. """ - summary: str = Field( - description="""A 2-4 sentence summary of your findings: what breaks in Firefox, + summary: Annotated[ + str, + Field( + description="""A 2-4 sentence summary of your findings: what breaks in Firefox, and how Chrome differs. Hard limit: 500 characters. State conclusions, not the investigation. Do NOT include: measurements or coordinates, script exit codes or pass/fail counts, how the script works, restatements of other result fields, or justification that the issue qualifies as webcompat.""" - ) + ), + ] - chrome_reproduced: bool | None = Field( - description=( - "Result of running the cross-check step in Chrome: " - "true if the issue also reproduces in Chrome, false if the " - "issue does not reproduce in Chrome, or null if the Chrome " - "cross-check wasn't able to confirm reproduction." + chrome_reproduced: Annotated[ + bool | None, + Field( + description=( + "Result of running the cross-check step in Chrome: " + "true if the issue also reproduces in Chrome, false if the " + "issue does not reproduce in Chrome, or null if the Chrome " + "cross-check wasn't able to confirm reproduction." + ), ), - ) + ] - steps: str = Field( - description=( - "The ordered steps you took, as a single numbered list (1., 2., 3., " - "... one step per line), written so another agent could reproduce " - "them with no extra context. Each step must be self-contained: " - "whenever you introduce an input or artifact the report did not " - "provide (a file, image, account, or any other test data), state its " - "exact origin — the URL you fetched it from, the command you ran, or " - 'how you generated it — not just that you "used" or "saved" it. A ' - "reader must be able to obtain the same inputs. Omit the Chrome cross-check " - "reproduction, Puppeteer script and screenshot steps." + steps: Annotated[ + str, + Field( + description=( + "The ordered steps you took, as a single numbered list (1., 2., 3., " + "... one step per line), written so another agent could reproduce " + "them with no extra context. Each step must be self-contained: " + "whenever you introduce an input or artifact the report did not " + "provide (a file, image, account, or any other test data), state its " + "exact origin — the URL you fetched it from, the command you ran, or " + 'how you generated it — not just that you "used" or "saved" it. A ' + "reader must be able to obtain the same inputs. Omit the Chrome cross-check " + "reproduction, Puppeteer script and screenshot steps." + ), ), - ) + ] - script_path: Path | None = Field( - description=( - """The file path of the Puppeteer confirmation script you wrote and + script_path: Annotated[ + Path | None, + Field( + description=( + """The file path of the Puppeteer confirmation script you wrote and ran successfully (exit code 0). Use the exact path you were given to write to (do NOT paste the script source). Must be null when `reproduced` is false.""" + ), ), - ) + ] @field_validator("script_path", mode="after") @classmethod @@ -209,14 +242,17 @@ def validate_script_matches_reproduced(self) -> BugReproductionResult: class ChromeMaskResult(BaseModel): - chrome_mask_fixed: bool | None = Field( - description=( - "Whether enabling the Chrome Mask extension (spoofing a Chrome " - "User-Agent) fixed the reported behavior: true if it fixed it, " - "false if it did not, null if the Chrome Mask test was not run " - "(e.g. the issue did not reproduce at baseline)." + chrome_mask_fixed: Annotated[ + bool | None, + Field( + description=( + "Whether enabling the Chrome Mask extension (spoofing a Chrome " + "User-Agent) fixed the reported behavior: true if it fixed it, " + "false if it did not, null if the Chrome Mask test was not run " + "(e.g. the issue did not reproduce at baseline)." + ), ), - ) + ] def build_result_server(collector: ResultCollector) -> McpServerConfig: diff --git a/bugbug/tools/code_review/data_types.py b/bugbug/tools/code_review/data_types.py index 07d2c002fb..0d0d632f69 100644 --- a/bugbug/tools/code_review/data_types.py +++ b/bugbug/tools/code_review/data_types.py @@ -1,4 +1,5 @@ import re +from typing import Annotated import httpx import tenacity @@ -11,48 +12,68 @@ class GeneratedReviewComment(BaseModel): """A review comment generated by the code review agent.""" - file: str = Field(description="The path to the file the comment applies to.") - code_line: int = Field(description="The line number that the comment refers to.") - comment: str = Field(description="The review comment.") - explanation: str = Field( - description="A brief rationale for the comment, including how confident you are and why." - ) - order: int = Field( - description="An integer representing the priority of the comment, with 1 being the highest confidence/importance." - ) + file: Annotated[ + str, Field(description="The path to the file the comment applies to.") + ] + code_line: Annotated[ + int, Field(description="The line number that the comment refers to.") + ] + comment: Annotated[str, Field(description="The review comment.")] + explanation: Annotated[ + str, + Field( + description="A brief rationale for the comment, including how confident you are and why." + ), + ] + order: Annotated[ + int, + Field( + description="An integer representing the priority of the comment, with 1 being the highest confidence/importance." + ), + ] class AgentResponse(BaseModel): """The response from the code review agent.""" - comments: list[GeneratedReviewComment] = Field( - description="A list of generated review comments." - ) + comments: Annotated[ + list[GeneratedReviewComment], + Field(description="A list of generated review comments."), + ] class PatchScopeResponse(BaseModel): """The response from the patch-scope assessment pass.""" - comments: list[GeneratedReviewComment] = Field( - description=( - "At most one comment suggesting the patch be split into smaller, " - "independently reviewable pieces. Empty when no split is warranted." - ) - ) + comments: Annotated[ + list[GeneratedReviewComment], + Field( + description=( + "At most one comment suggesting the patch be split into smaller, " + "independently reviewable pieces. Empty when no split is warranted." + ) + ), + ] class CodeReviewToolResponse(BaseModel): """The response from the CodeReviewTool.""" - review_comments: list[InlineComment] = Field( - description="A list of inline comments to be added to the code review, converted from the agent's generated comments." - ) - patch_summary: str = Field( - description="A brief summary of the patch, generated by the agent." - ) - details: dict = Field( - description="Additional details about the tool's execution, such as which models were used and any relevant metadata." - ) + review_comments: Annotated[ + list[InlineComment], + Field( + description="A list of inline comments to be added to the code review, converted from the agent's generated comments." + ), + ] + patch_summary: Annotated[ + str, Field(description="A brief summary of the patch, generated by the agent.") + ] + details: Annotated[ + dict, + Field( + description="Additional details about the tool's execution, such as which models were used and any relevant metadata." + ), + ] class SkillLoadError(Exception): @@ -69,13 +90,16 @@ def _strip_frontmatter(text: str) -> str: class Skill(BaseModel): """A reusable instruction set the agent can load on demand.""" - name: str = Field( - description="A unique identifier the agent uses to load the skill." - ) - url: str = Field(description="HTTPS URL of the skill.md file.") - description: str = Field( - description="Short summary shown to the agent so it can decide when to load this skill." - ) + name: Annotated[ + str, Field(description="A unique identifier the agent uses to load the skill.") + ] + url: Annotated[str, Field(description="HTTPS URL of the skill.md file.")] + description: Annotated[ + str, + Field( + description="Short summary shown to the agent so it can decide when to load this skill." + ), + ] _cached_body: str | None = PrivateAttr(default=None) @@ -115,11 +139,13 @@ async def _fetch_url(url: str) -> httpx.Response: class ExternalContent(BaseModel): """An external file fetched and injected as context for the review.""" - name: str = Field(description="A unique identifier for this content item.") - url: str = Field(description="HTTPS URL of the file to fetch.") - description: str = Field( - description="Short description of what this content provides." - ) + name: Annotated[ + str, Field(description="A unique identifier for this content item.") + ] + url: Annotated[str, Field(description="HTTPS URL of the file to fetch.")] + description: Annotated[ + str, Field(description="Short description of what this content provides.") + ] _cached_body: str | None = PrivateAttr(default=None) diff --git a/bugbug/tools/comment_matching/agent.py b/bugbug/tools/comment_matching/agent.py index ce76a7ab80..e2724b7d21 100644 --- a/bugbug/tools/comment_matching/agent.py +++ b/bugbug/tools/comment_matching/agent.py @@ -1,3 +1,5 @@ +from typing import Annotated + from langchain.agents import create_agent from langchain.agents.structured_output import ProviderStrategy from langchain.chat_models import BaseChatModel, init_chat_model @@ -9,20 +11,25 @@ class MatchingComment(BaseModel): - id: int = Field(description="Unique identifier for the comment") - content: str = Field(description="Content of the code review comment") - file: str = Field(description="File path of the comment") + id: Annotated[int, Field(description="Unique identifier for the comment")] + content: Annotated[str, Field(description="Content of the code review comment")] + file: Annotated[str, Field(description="File path of the comment")] class CommentMatch(BaseModel): - old_comment_id: int = Field(description="ID of the comment from the old set") - new_comment_id: int = Field(description="ID of the comment from the new set") + old_comment_id: Annotated[ + int, Field(description="ID of the comment from the old set") + ] + new_comment_id: Annotated[ + int, Field(description="ID of the comment from the new set") + ] class AgentResponse(BaseModel): - matches: list[CommentMatch] = Field( - description="List of matched comment pairs between old and new comments" - ) + matches: Annotated[ + list[CommentMatch], + Field(description="List of matched comment pairs between old and new comments"), + ] class CommentMatchingTool: diff --git a/bugbug/tools/suggestion_filtering/agent.py b/bugbug/tools/suggestion_filtering/agent.py index f6c542ca89..8216b4d91c 100644 --- a/bugbug/tools/suggestion_filtering/agent.py +++ b/bugbug/tools/suggestion_filtering/agent.py @@ -5,7 +5,7 @@ """Suggestion filtering tool implementation.""" -from typing import Iterable, Optional +from typing import Annotated, Iterable, Optional from langchain.agents import create_agent from langchain.agents.structured_output import ProviderStrategy @@ -26,9 +26,12 @@ class FilteredComments(BaseModel): """The response from the filtering agent.""" - comment_indices: list[int] = Field( - description="A list of indices of the comments that were kept after filtering" - ) + comment_indices: Annotated[ + list[int], + Field( + description="A list of indices of the comments that were kept after filtering" + ), + ] class SuggestionFilteringTool(GenerativeModelTool): diff --git a/libs/hackbot-runtime/hackbot_runtime/actions/testrail.py b/libs/hackbot-runtime/hackbot_runtime/actions/testrail.py index fa4ddd5a95..3edbe30c82 100644 --- a/libs/hackbot-runtime/hackbot_runtime/actions/testrail.py +++ b/libs/hackbot-runtime/hackbot_runtime/actions/testrail.py @@ -19,20 +19,26 @@ class TestRailStepInput(BaseModel): - action: str = Field(description="Test step action.") - expectation: str | None = Field( - default=None, - description=("Expected result for this step."), - ) + action: Annotated[str, Field(description="Test step action.")] + expectation: Annotated[ + str | None, + Field( + default=None, + description=("Expected result for this step."), + ), + ] class TestRailCaseResultInput(BaseModel): status: Literal["passed", "failed", "unsuitable"] summary: str - failure_reason: str | None = Field( - default=None, - description="Required when status is failed or unsuitable.", - ) + failure_reason: Annotated[ + str | None, + Field( + default=None, + description="Required when status is failed or unsuitable.", + ), + ] @model_validator(mode="after") def failure_reason_required_for_non_passing_cases( @@ -45,10 +51,14 @@ def failure_reason_required_for_non_passing_cases( class TestRailCaseInput(BaseModel): id: int - title: str = Field(description="TestRail test case title.") - preconditions: str | None = Field( - default=None, description="Optional setup required before running this case." - ) + title: Annotated[str, Field(description="TestRail test case title.")] + preconditions: Annotated[ + str | None, + Field( + default=None, + description="Optional setup required before running this case.", + ), + ] steps: Annotated[ list[TestRailStepInput], Field( @@ -59,11 +69,14 @@ class TestRailCaseInput(BaseModel): ), ), ] - result: TestRailCaseResultInput = Field( - description=( - "Execution result for this generated test case after the agent ran it." - ) - ) + result: Annotated[ + TestRailCaseResultInput, + Field( + description=( + "Execution result for this generated test case after the agent ran it." + ) + ), + ] @field_validator("title") @classmethod @@ -90,7 +103,9 @@ def expectations_must_include_verification(self) -> "TestRailCaseInput": class SubmitTestPlanInput(BaseModel): - feature: str = Field(description="Feature covered by the generated test cases.") + feature: Annotated[ + str, Field(description="Feature covered by the generated test cases.") + ] generated_test_cases: Annotated[ list[TestRailCaseInput], Field( @@ -99,10 +114,13 @@ class SubmitTestPlanInput(BaseModel): description="Generated test cases to upload to TestRail.", ), ] - summary: str | None = Field( - default=None, - description="Optional summary of the generated test-plan execution.", - ) + summary: Annotated[ + str | None, + Field( + default=None, + description="Optional summary of the generated test-plan execution.", + ), + ] @field_validator("feature") @classmethod diff --git a/libs/hackbot-runtime/hackbot_runtime/context.py b/libs/hackbot-runtime/hackbot_runtime/context.py index e954e7daba..c2df7197bd 100644 --- a/libs/hackbot-runtime/hackbot_runtime/context.py +++ b/libs/hackbot-runtime/hackbot_runtime/context.py @@ -19,7 +19,7 @@ import uuid from functools import cached_property from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Annotated from pydantic import Field, PrivateAttr from pydantic_settings import BaseSettings, SettingsConfigDict @@ -60,7 +60,7 @@ class HackbotContext(BaseSettings): uploaded. """ - run_id: str = Field(default_factory=_default_run_id) + run_id: Annotated[str, Field(default_factory=_default_run_id)] results_prefix: str = "" results_policy_url: str | None = None results_policy_fields: dict[str, str] = {} diff --git a/libs/lando-client/lando_client/config.py b/libs/lando-client/lando_client/config.py index 9aaece5bc7..50b8718940 100644 --- a/libs/lando-client/lando_client/config.py +++ b/libs/lando-client/lando_client/config.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Annotated from urllib.parse import urlsplit from pydantic import BaseModel, Field, model_validator @@ -27,7 +28,7 @@ class LandoSettings(BaseModel): mode), and the try repository's permissions are checked against that user. """ - access_token: str = Field(min_length=1) + access_token: Annotated[str, Field(min_length=1)] url: str = "https://lando.moz.tools" instance_id: str | None = None timeout_seconds: int = 60 diff --git a/libs/phabricator-client/phabricator_client/config.py b/libs/phabricator-client/phabricator_client/config.py index 5d8772a2c5..e684f91c55 100644 --- a/libs/phabricator-client/phabricator_client/config.py +++ b/libs/phabricator-client/phabricator_client/config.py @@ -8,12 +8,14 @@ from __future__ import annotations +from typing import Annotated + from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict class PhabricatorSettings(BaseModel): - api_key: str = Field(min_length=32, max_length=32) + api_key: Annotated[str, Field(min_length=32, max_length=32)] url: str = "https://phabricator.services.mozilla.com" timeout_seconds: int = 60 diff --git a/libs/phabricator-client/phabricator_client/models.py b/libs/phabricator-client/phabricator_client/models.py index 59f03fb739..f9e8cd053b 100644 --- a/libs/phabricator-client/phabricator_client/models.py +++ b/libs/phabricator-client/phabricator_client/models.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Annotated + from pydantic import BaseModel, ConfigDict, Field @@ -15,4 +17,6 @@ class PhabricatorDiff(BaseModel): model_config = ConfigDict(populate_by_name=True, extra="ignore") id: int - base_commit: str | None = Field(default=None, alias="sourceControlBaseRevision") + base_commit: Annotated[ + str | None, Field(default=None, alias="sourceControlBaseRevision") + ] diff --git a/libs/testrail-client/testrail_client/config.py b/libs/testrail-client/testrail_client/config.py index fa534d7940..9672279c25 100644 --- a/libs/testrail-client/testrail_client/config.py +++ b/libs/testrail-client/testrail_client/config.py @@ -2,13 +2,15 @@ from __future__ import annotations +from typing import Annotated + from pydantic import BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict class TestRailSettings(BaseModel): - username: str = Field(min_length=1) - api_key: str = Field(min_length=1) + username: Annotated[str, Field(min_length=1)] + api_key: Annotated[str, Field(min_length=1)] project_id: int url: str = "https://mozilla.testrail.io" timeout_seconds: int = 30 diff --git a/services/hackbot-api/app/schemas.py b/services/hackbot-api/app/schemas.py index 49e48d48a9..c22bc4fe9d 100644 --- a/services/hackbot-api/app/schemas.py +++ b/services/hackbot-api/app/schemas.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum -from typing import Any +from typing import Annotated, Any from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -23,8 +23,8 @@ class ArtifactRef(BaseModel): class RunSummary(BaseModel): status: str error: str | None = None - findings: dict[str, Any] = Field(default_factory=dict) - actions: list[dict[str, Any]] = Field(default_factory=list) + findings: Annotated[dict[str, Any], Field(default_factory=dict)] + actions: Annotated[list[dict[str, Any]], Field(default_factory=list)] class RunActionDoc(BaseModel): @@ -69,7 +69,7 @@ class RunDoc(BaseModel): execution_name: str | None = None results_prefix: str summary: RunSummary | None = None - artifacts: list[ArtifactRef] = Field(default_factory=list) + artifacts: Annotated[list[ArtifactRef], Field(default_factory=list)] error: str | None = None @@ -85,7 +85,7 @@ class BugFixInputs(BaseModel): comment: str | None = None # Set only by a Bugzilla flag.needinfo webhook. Its presence selects the # follow-up mode and lets the API clear that exact flag after the response. - bugzilla_needinfo_flag_id: int | None = Field(default=None, gt=0) + bugzilla_needinfo_flag_id: Annotated[int | None, Field(default=None, gt=0)] model: str | None = None max_turns: int | None = None effort: str | None = None diff --git a/services/hackbot-api/app/slack_webhook.py b/services/hackbot-api/app/slack_webhook.py index b2d7b8a468..1ce51a2ed3 100644 --- a/services/hackbot-api/app/slack_webhook.py +++ b/services/hackbot-api/app/slack_webhook.py @@ -10,7 +10,7 @@ from __future__ import annotations import logging -from typing import Any, Literal +from typing import Annotated, Any, Literal from pydantic import BaseModel, Field, Json @@ -40,7 +40,7 @@ class Message(BaseModel): class ActionValue(BaseModel): type: Literal["start_agent_run"] agent_name: str - params: dict[str, Any] = Field(default_factory=dict) + params: Annotated[dict[str, Any], Field(default_factory=dict)] class Action(BaseModel): @@ -67,6 +67,6 @@ class BlockActionsEvent(BaseModel): user: User channel: Channel | None = None message: Message | None = None - actions: list[Action] = Field(default_factory=list) + actions: Annotated[list[Action], Field(default_factory=list)] response_url: str | None = None trigger_id: str diff --git a/services/mcp/src/bugbug_mcp/server.py b/services/mcp/src/bugbug_mcp/server.py index a61cf2f9a8..dc0e1dc050 100644 --- a/services/mcp/src/bugbug_mcp/server.py +++ b/services/mcp/src/bugbug_mcp/server.py @@ -35,7 +35,10 @@ def get_code_review_tool(): @mcp.prompt() async def patch_review( - patch_url: str = Field(description="URL to the Phabricator patch to review."), + patch_url: Annotated[ + str, + Field(description="URL to the Phabricator patch to review."), + ], ) -> str: """Review a code patch from Phabricator.""" parsed_url = urlparse(patch_url)