Refactor into files with structured output

This commit is contained in:
Shrikanth Upadhayaya 2025-03-27 10:09:06 -04:00
parent 6621a67730
commit e07618e391
No known key found for this signature in database
9 changed files with 486 additions and 290 deletions

View File

@ -1 +1 @@
3.11
3.13

View File

@ -1,4 +1,4 @@
FROM python:3.11-bookworm
FROM python:3.13-bookworm
RUN mkdir /app
COPY ./requirements.txt /app/requirements.txt

149
final_agent.py Normal file
View File

@ -0,0 +1,149 @@
import json
import logging
from dataclasses import dataclass
from typing import Optional
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from pydantic_ai.agent import AgentRunResult
from pydantic_ai.settings import ModelSettings
from lib import run_agent_with_retries
from models import Comment
@dataclass
class FinalAgentDeps:
"""
The context for the final review agent.
"""
logger: logging.Logger
class FinalResponse(BaseModel):
"""
The response to the final review of the schematic.
"""
overview: str = Field(
...,
description=(
"An overview of your review, summarizing the key issues and improvements."
"This is the equivalent of your top-level comment on a code review. This"
"must be in GitHub Flavored Markdown."
),
)
comments: list[Comment] = Field(
...,
description="A list of comments to be made on the design and on all pages.",
)
FINAL_REVIEW_PROMPT = f"""
You are a senior Schematic Engineer. You have been given a schematic to review.
You are to provide feedback on the schematic to the junior engineer who designed
it. You have ALREADY reviewed all the pages of the schematic and have access to:
1. The netlist of the complete design, which includes all the nets and their
connections across ALL pages;
2. Your written notes from your review of each page; and
3. A list of comments you have made on each page of the schematic.
Your goal is to provide a final review of the schematic, focusing on the
overall design, the connections between different pages, your knowledge of best
practices in schematic design and any issues that you have found. You should
provide a detailed review that the junior engineer can use to improve the
schematic. The netlist may be blank. If it is, review the schematic without
considering the netlist.
First, review the page in <thinking></thinking> tags.
Once you are done thinking, respond in <output> tags in JSON following the
given schema:
{json.dumps(FinalResponse.model_json_schema(), indent=4)}
Your response should be JUST a COMPLETE, VALID JSON document and NOTHING ELSE.
You MUST NOT have ANY other text, including ```json or similar.
1. Write an overview of your review, summarizing the key issues and improvements
in the schematic. This is the equivalent of your top-level comment on a code
review. Do NOT summarize the design itself; your overview should be about the
review process and the issues you found.
2. Provide a list of comments to be made on the design. These comments should
be actionable, specific, and should cover all the issues you found in the
schematic. Make sure to include the page number, the position of the issue on
the page, the element being commented on, and the importance of the issue.
This doesn't mean you should repeat all the comments you made on each page;
instead, synthesize them into a final review with the most important and
relevant issues. Sometimes, issues may not be relevant any more after you
have reviewed more pages; in that case, you can leave them out.
When drafting your response, be confident, clear and concise. Avoid weasel
words and passive voice. Your review should be detailed and actionable, and
should provide the junior engineer with a clear path to improving the
schematic. Your text should directly address the creator of the design in the
style typical of a code review. If you have NO such advice, say so and
congratulate the junior engineer on a job well done.
Notes for your review:
1. Do NOT hallucinate. Focus on elements of the schematic that are actually on
the design, and use your notes and the netlist for details and references.
1. Your suggestions must NOT require a human to further review. For example,
instead of saying "Ensure all ICs have adequate decoupling capacitors", you
should check that yourself and provide the results.
2. DO NOT start any suggestions with "Ensure", "Confirm", "Verify" or similar
words; all recommendations MUST BE specific, actionable and relevant to the
design.
3. Your recommendations SHOULD include a reference to WHAT component, pin or
net should be reviewed.
4. The text in your comments MUST be in markdown.
5. You have access to a tool to give you more time to think about the
schematic. Use it if you need to.
"""
final_agent = Agent(
system_prompt=FINAL_REVIEW_PROMPT,
deps_type=FinalAgentDeps,
result_type=FinalResponse,
model_settings=ModelSettings(max_tokens=8192),
)
@final_agent.tool()
async def think_tool(ctx: RunContext[FinalAgentDeps]):
"""
A tool to give the agent more time to think about the schematic.
"""
ctx.deps.logger.debug("Thinking.")
return "Hmm."
async def call(
memory: str,
comments: list[Comment],
netlist: str,
deps: FinalAgentDeps,
max_attempts: int,
retry_delay: int,
logger: logging.Logger,
) -> Optional[AgentRunResult[FinalResponse]]:
return await run_agent_with_retries(
final_agent,
[
"Provide a final review of the schematic.",
f"Memory: {memory}",
f"Netlist: {netlist}",
f"Comments: {json.dumps([comment.model_dump_json() for comment in comments])}",
],
deps,
max_attempts,
retry_delay,
logger,
)

50
lib.py
View File

@ -1,10 +1,16 @@
# cSpell:words polylines beziers
# cSpell:words polylines beziers pydai
from typing import Any
import asyncio
import logging
from typing import Any, Optional, Sequence
from xml.etree import ElementTree as ET
from playwright.async_api import async_playwright
from pydantic_ai import Agent
from pydantic_ai import exceptions as pydai_exceptions
from pydantic_ai.agent import AgentRunResult
from pydantic_ai.messages import UserContent
def filter_component_details(json: dict) -> dict:
@ -28,11 +34,13 @@ def filter_component_details(json: dict) -> dict:
"beziers",
"bitmaps",
"texts",
"position",
]:
if key in filtered:
del filtered[key]
if "position" in json:
filtered["position"] = [json["position"].get("x"), json["position"].get("y")]
if "attributes" in json:
filtered["attributes"] = {}
for key, attr in json["attributes"].items():
@ -44,6 +52,10 @@ def filter_component_details(json: dict) -> dict:
filtered["pins"][pin_id] = {
"designator": pin.get("designator"),
"electrical_type": pin.get("electrical_type"),
"position": [
pin.get("position").get("x"),
pin.get("position").get("y"),
],
}
return filtered
@ -184,3 +196,35 @@ async def render_svg(svg_path: str, output_path: str) -> None:
await page.screenshot(path=output_path, full_page=False)
await context.close()
await browser.close()
async def run_agent_with_retries[DT, RT](
agent: Agent[DT, RT],
inputs: Sequence[UserContent],
deps: DT,
max_attempts: int,
retry_delay: int,
logger: logging.Logger,
) -> Optional[AgentRunResult[RT]]:
attempts = 0
result = None
while attempts < max_attempts:
try:
result = await agent.run(inputs, deps=deps)
break
except pydai_exceptions.ModelHTTPError as e:
if e.status_code in [429, 529]:
attempts += 1
cause = "Rate limited" if e.status_code == 429 else "Overloaded"
logger.warning(
f"{cause}; sleeping {retry_delay}s before "
f"retrying attempt {attempts}"
)
logger.debug(f"Error: {e}")
await asyncio.sleep(retry_delay)
continue
else:
raise e
return result

309
main.py
View File

@ -1,225 +1,35 @@
# cspell:words pydai
import argparse
import asyncio
import enum
import json
import logging
import os
import sys
import tempfile
from typing import Optional, Sequence
from pydantic import BaseModel, Field
from pydantic_ai import Agent, BinaryContent
from pydantic_ai import exceptions as pydai_exceptions
from pydantic_ai.messages import UserContent
import final_agent
import step_agent
from lib import filter_schematic_page, render_svg, split_multipage_svg
VERSION = (0, 3, 0)
VERSION = (0, 4, 0)
SLEEP_WHEN_RATE_LIMITED = 60
RETRY_DELAY = 60
"""
Time in seconds to sleep between pages to when hitting a rate limit.
"""
RATE_LIMIT_ATTEMPTS = 3
MAX_ATTEMPTS = 3
"""
Number of attempts to make when hitting a rate limit.
"""
DEFAULT_MODEL = "anthropic:claude-3-7-sonnet-latest"
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
STEP_PROMPT = """
You are a senior Schematic Engineer. You have been given a schematic to review.
You are to provide feedback on the schematic to the junior engineer who designed
it. You currently are reviewing a single page of the schematic, and are
proceeding in a page-by-page manner. For this page, you have access to:
1. The netlist of the complete design, which includes all the nets and their
connections across ALL pages;
2. A high-resolution image of the schematic page you are reviewing.
3. A JSON representation of the schematic page you are reviewing.
4. Written notes (memory) from your review so far.
Your goal is to review this page, keeping in mind the overall design, your
knowledge of best practices in schematic design, and your memory so far. The
netlist may be blank. If it is, review the schematic without considering the
netlist.
Once you review the page, you should respond with extensive notes considering
everything you have seen so far and the new page you are reviewing. Your
response will be your memory for the next page. Note that you'll lose your
current memory once you move on to the next page, so your memory should include
everything you need to remember from the current notes as well. You should NOT
provide feedback on the overall design yet. Be as detailed as possible in your
memory. Especially retain references to specific elements of the schematic
which you may need to refer back to, such as designators of components or pins,
net names, etc.
Eventually, once you have reviewed all pages, you will provide a final review
covering the entire schematic, so make sure to keep track of all the important
issues you find. At that time, you will only have access to the netlist and your
written notes, so you MUST ensure that your notes are detailed enough to cover
all the issues you find, with potential suggestions for solutions.
Notes for your review:
1. Do NOT hallucinate. Focus on elements of the schematic that are actually on
the page, and make sure to include details to keep track of these elements
in your notes.
2. Do NOT provide feedback on the overall design of the schematic yet. You will
do that once you have reviewed all pages.
3. Use as much written memory as you need to keep track of details.
4. Your suggestions must NOT require a human to further review. For example,
instead of saying "Ensure all ICs have adequate decoupling capacitors", you
should check that yourself and provide the results. Make sure your notes will
help you in this regard.
"""
step_agent = Agent(system_prompt=STEP_PROMPT)
class Comment(BaseModel):
"""
A single comment to be made on this page of the design.
"""
class Importance(enum.Enum):
CRITICAL = "Critical"
MAJOR = "Major"
MINOR = "Minor"
TRIVIAL = "Trivial"
summary: str = Field(
..., description="A 1-2 line summary of the issue/improvement."
)
page: Optional[int] = Field(
None,
description=(
"The page number that this comment is about. If the comment is about"
"a general issue, you can leave this blank."
),
)
element: Optional[str] = Field(
None,
description=(
"The component, pin or net that should be reviewed. If the"
"comment is about a general issue, you can leave this blank."
),
)
suggestion: Optional[str] = Field(
None,
description=(
"If you have a concrete, actionable suggestion, provide it here."
"Otherwise, you can leave this blank."
),
)
importance: Importance = Field(..., description="The importance of the comment.")
class FinalResponse(BaseModel):
"""
The response type for the agent.
"""
overview: str = Field(..., description="An overview of the review.")
comment: list[Comment] = Field(
..., description="A list of comments to be made on the design."
)
FINAL_REVIEW_PROMPT = """
You are a senior Schematic Engineer. You have been given a schematic to review.
You are to provide feedback on the schematic to the junior engineer who designed
it. You have reviewed all the pages of the schematic and have access to:
1. The netlist of the complete design, which includes all the nets and their
connections across ALL pages; and
2. Your written notes from your review of each page.
If the schematic has only one page, you will not have any notes and instead
additionally have access to:
1. A high-resolution image of the schematic page you are reviewing.
2. A JSON representation of the schematic page you are reviewing.
Your goal is to provide a final review of the schematic, focusing on the
overall design, the connections between different pages, your knowledge of best
practices in schematic design and any issues that you have found. You should
provide a detailed review that the junior engineer can use to improve the
schematic. The netlist may be blank. If it is, review the schematic without
considering the netlist.
Your review should be in markdown, and should include the following:
1. An overview of your review, summarizing the key issues and improvements.
Consider this the equivalent of your top-level comment on a code review.
2. A list of comments, each with a summary, a page (if applicable), an element
(if applicable), a suggestion (if applicable), and an importance level. These
would be the equivalent of line comments on a code review, and will be
attached to specific elements or pages of the schematic.
Notes for your review:
1. Do NOT hallucinate. Focus on elements of the schematic that are actually on
the design, and use your notes and the netlist for details and references.
1. Your suggestions must NOT require a human to further review. For example,
instead of saying "Ensure all ICs have adequate decoupling capacitors", you
should check that yourself and provide the results.
2. DO NOT start any suggestions with "Ensure", "Confirm", "Verify" or similar
words; all recommendations MUST BE specific, actionable and relevant to the
design.
3. Your recommendations SHOULD include a reference to WHAT component, pin or
net should be reviewed.
4. The text in your comments MUST be in markdown.
"""
final_agent = Agent(system_prompt=FINAL_REVIEW_PROMPT)
# @final_agent.tool_plain()
# async def think_tool():
# """
# A tool to give the agent more time to think about the schematic.
# """
# logger.debug("Thinking.")
# return "Think"
async def run_agent_with_retries(agent: Agent, inputs: Sequence[UserContent]):
attempts = 0
result = None
while attempts < RATE_LIMIT_ATTEMPTS:
try:
result = await agent.run(inputs)
break
except pydai_exceptions.ModelHTTPError as e:
if e.status_code in [429, 529]:
attempts += 1
cause = "Rate limited" if e.status_code == 429 else "Overloaded"
logger.warning(
f"{cause}; sleeping {SLEEP_WHEN_RATE_LIMITED}s before "
f"retrying attempt {attempts}"
)
logger.debug(f"Error: {e}")
await asyncio.sleep(SLEEP_WHEN_RATE_LIMITED)
continue
else:
raise e
return result
async def main():
arg_parser = argparse.ArgumentParser(description="LLM Review")
@ -236,7 +46,7 @@ async def main():
arg_parser.add_argument(
"--model",
help="Model to use for the agent",
default="anthropic:claude-3-5-sonnet-latest",
default=DEFAULT_MODEL,
)
arg_parser.add_argument("--log-level", help="Log level", default="INFO")
@ -281,70 +91,63 @@ async def main():
with open(args.netlist_path, "r") as f:
netlist = f.read()
if args.model and args.model != "__default__":
step_agent.model = args.model
final_agent.model = args.model
if args.model == "__default__":
args.model = DEFAULT_MODEL
step_agent.step_agent.model = args.model
final_agent.final_agent.model = args.model
current_memory = ""
current_comments = []
if len(page_paths) == 1:
logger.info("Only one page, running final review now.")
else:
for i, (page_json_path, page_png_path) in enumerate(page_paths):
logger.info(f"Reviewing page {i + 1}/{len(page_paths)}")
for i, (page_json_path, page_png_path) in enumerate(page_paths):
logger.info(f"Reviewing page {i + 1}/{len(page_paths)}")
with open(page_json_path, "r") as f:
page_json = f.read()
with open(page_png_path, "rb") as f:
image = f.read()
try:
result = await run_agent_with_retries(
step_agent,
[
f"You are on page {i + 1} of {len(page_paths)}. Your notes so"
f"far: {current_memory}\n The netlist, JSON and image are attached.",
f"Netlist: {netlist}",
f"JSON: {page_json}",
BinaryContent(data=image, media_type="image/png"),
],
)
except Exception as e:
logger.error(f"Error running agent: {e}")
sys.exit(1)
if result is None:
logger.error("Could not get a response from the API. Exiting")
sys.exit(1)
current_memory = result.data
logger.info(f"Completed review of page {i + 1}")
logger.debug(f"Memory: {current_memory}")
logger.info("Completed review of all pages, preparing final comment.")
final_result_args: list[UserContent] = [
f"Netlist: {netlist}",
]
if len(page_paths) == 1:
page_json_path, page_png_path = page_paths[0]
with open(page_json_path, "r") as f:
page_json = f.read()
with open(page_png_path, "rb") as f:
image = f.read()
final_result_args.extend(
[
f"JSON: {page_json}",
BinaryContent(data=image, media_type="image/png"),
]
)
else:
final_result_args.append(f"Your notes from the page reviews: {current_memory}")
try:
result = await step_agent.call(
current_memory,
current_comments,
page_json,
image,
netlist,
i + 1,
len(page_paths),
MAX_ATTEMPTS,
RETRY_DELAY,
logger,
)
except Exception as e:
logger.error(f"Error running agent: {e}")
sys.exit(1)
if result is None:
logger.error("Could not get a response from the API. Exiting")
sys.exit(1)
current_memory = result.memory
current_comments.extend(result.comments)
logger.info(f"Completed review of page {i + 1}")
logger.debug(f"Memory: {current_memory}")
logger.debug(f"Comments: {current_comments}")
logger.info("Completed review of all pages, preparing final comment.")
try:
final_result = await run_agent_with_retries(final_agent, final_result_args)
final_result = await final_agent.call(
current_memory,
current_comments,
netlist,
final_agent.FinalAgentDeps(logger=logger),
MAX_ATTEMPTS,
RETRY_DELAY,
logger,
)
except Exception as e:
logger.error(f"Error running agent: {e}")
sys.exit(1)
@ -354,7 +157,7 @@ async def main():
sys.exit(1)
with open(args.output_path, "w") as f:
f.write(final_result.data)
f.write(final_result.data.model_dump_json(indent=4))
temp_dir.cleanup()

66
models.py Normal file
View File

@ -0,0 +1,66 @@
# cspell:words viewbox
import enum
from typing import Optional
from pydantic import BaseModel, Field
class Importance(enum.Enum):
CRITICAL = "Critical"
MAJOR = "Major"
MINOR = "Minor"
TRIVIAL = "Trivial"
class Comment(BaseModel):
"""
A comment on a Design Review for a design.
"""
summary: str = Field(
...,
description=(
"A 1-2 line summary of the issue/improvement in GitHub Flavored Markdown."
),
)
page: int = Field(
...,
description=(
"The page number that this comment is about. When critiquing a "
"specific page, you MUST provide the page number. You can set this "
"field to 0 ONLY if the comment is about the overall design."
),
)
element: Optional[str] = Field(
None,
description=(
"The component, pin or net that should be reviewed. If the comment is "
"about a general issue, you can leave this blank."
),
)
viewbox: Optional[list[float]] = Field(
None,
description=(
"A viewbox to highlight for this comment. The viewbox should be in "
"the form of [bottom left x, bottom left y, top right x, top right "
"y]. To construct a viewbox, consider the positions of the elements "
"you are critiquing in the JSON for the page. If your comment is "
"about a specific element (i.e. component, net, wire or pin) or "
"group of elements, you MUST provide a viewbox. Otherwise, you can "
"leave this blank."
),
)
suggestion: Optional[str] = Field(
None,
description=(
"If you have a concrete, actionable suggestion, provide it here in "
"GitHub Flavored Markdown. Otherwise, you can leave this blank."
),
)
importance: Importance = Field(..., description="The importance of the comment.")

View File

@ -2,7 +2,7 @@
[project]
name = "llm-review"
version = "0.3.0"
version = "0.4.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11"

134
step_agent.py Normal file
View File

@ -0,0 +1,134 @@
import json
import logging
from typing import Optional
from pydantic import BaseModel, Field
from pydantic_ai import Agent, BinaryContent
from pydantic_ai.agent import AgentRunResult
from pydantic_ai.settings import ModelSettings
from lib import run_agent_with_retries
from models import Comment
class StepResponse(BaseModel):
"""
The response to a single step in the design review process.
"""
memory: str = Field(
...,
description=(
"The memory to be used for the next page in the review, including "
"your notes from this page."
),
)
comments: list[Comment] = Field(
...,
description="A list of comments to be made on the design for this page.",
)
class ResponseWithThinking(BaseModel):
thinking: str
output: StepResponse
STEP_PROMPT = f"""
You are a senior Schematic Engineer. You have been given a schematic to review.
You are to provide feedback on the schematic to the junior engineer who designed
it. You currently are reviewing a single page of the schematic, and are
proceeding in a page-by-page manner. For this page, you have access to:
1. The netlist of the complete design, which includes all the nets and their
connections across ALL pages;
2. A high-resolution image of the schematic page you are reviewing.
3. A JSON representation of the schematic page you are reviewing.
4. Written notes (memory) from your review so far.
5. A set of comments you have made on previous pages of the schematic.
Your goal is to review this page, keeping in mind the overall design, your
knowledge of best practices in schematic design, and your memory so far.
Consider the purpose of the design, your notes so far, and common issues or
good to haves in schematic design. The netlist may be blank. If it is, review
the schematic without considering the netlist.
First, review the page in <thinking></thinking> tags.
Once you are done thinking, respond in <output> tags in JSON following the
given schema:
{json.dumps(StepResponse.model_json_schema(), indent=4)}
Your output MUST be JUST a COMPLETE, VALID JSON document and NOTHING ELSE.
You MUST NOT have ANY other text, including ```json or similar.
1. Your memory should be comprehensive, detailed and accurate, and should be
sufficient to conduct the final review of this page WITHOUT access to the
JSON or the image.
2. Your comments should be actionable, specific, and should cover all the
issues you find on the page. Make sure to include the page number, the
position of the issue on the page, the element being commented on, and the
importance of the issue.
Eventually, once you have reviewed all pages, you will provide a final review
covering the entire schematic, so make sure to keep track of all the important
issues you find. At that time, you will only have access to the netlist and your
written notes, so you MUST ensure that your notes are detailed enough to cover
all the issues you find, with potential suggestions for solutions.
Notes for your review:
1. Do NOT hallucinate. Focus on elements of the schematic that are actually on
the page, and make sure to include details to keep track of these elements
in your notes.
2. Do NOT provide feedback on the overall design of the schematic yet. You will
do that once you have reviewed all pages.
3. Use as much written memory as you need to keep track of details.
4. Your suggestions must NOT require a human to further review. For example,
instead of saying "Ensure all ICs have adequate decoupling capacitors", you
should check that yourself and provide the results. Make sure your notes will
help you in this regard.
"""
step_agent = Agent(
system_prompt=STEP_PROMPT,
result_type=ResponseWithThinking,
model_settings=ModelSettings(max_tokens=8192),
)
async def call(
memory: str,
comments: list[Comment],
page_json: str,
page_image: bytes,
netlist: str,
page_number: int,
total_pages: int,
max_attempts: int,
retry_delay: int,
logger: logging.Logger,
) -> Optional[StepResponse]:
response = await run_agent_with_retries(
step_agent,
[
f"Review this page. You are on page {page_number} of {total_pages}.",
f"Page JSON: {page_json}",
BinaryContent(page_image, media_type="image/png"),
f"Netlist: {netlist}",
f"Memory: {memory}",
f"Comments: {json.dumps([comment.model_dump_json() for comment in comments])}",
],
deps=None,
max_attempts=max_attempts,
retry_delay=retry_delay,
logger=logger,
)
if response:
return response.data.output
else:
return None

62
uv.lock generated
View File

@ -44,39 +44,39 @@ wheels = [
[[package]]
name = "argcomplete"
version = "3.6.0"
version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/be/29abccb5d9f61a92886a2fba2ac22bf74326b5c4f55d36d0a56094630589/argcomplete-3.6.0.tar.gz", hash = "sha256:2e4e42ec0ba2fff54b0d244d0b1623e86057673e57bafe72dda59c64bd5dee8b", size = 73135 }
sdist = { url = "https://files.pythonhosted.org/packages/0a/35/aacd2207c79d95e4ace44292feedff8fccfd8b48135f42d84893c24cc39b/argcomplete-3.6.1.tar.gz", hash = "sha256:927531c2fbaa004979f18c2316f6ffadcfc5cc2de15ae2624dfe65deaf60e14f", size = 73474 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/94/e786d91ccc3a1fc664c20332825b73da20928eb067cdc984b821948a1acc/argcomplete-3.6.0-py3-none-any.whl", hash = "sha256:4e3e4e10beb20e06444dbac0ac8dda650cb6349caeefe980208d3c548708bedd", size = 43769 },
{ url = "https://files.pythonhosted.org/packages/f5/c8/9fa0e6fa97c328d44e089278399b0a1a08268b06a4a71f7448c6b6effb9f/argcomplete-3.6.1-py3-none-any.whl", hash = "sha256:cef54d7f752560570291214f0f1c48c3b8ef09aca63d65de7747612666725dbc", size = 43984 },
]
[[package]]
name = "boto3"
version = "1.37.18"
version = "1.37.20"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
sdist = { url = "https://files.pythonhosted.org/packages/87/42/2b102f999c76614e55afd8a8c2392c35ce2f390cdeb78007aba029cd1171/boto3-1.37.18.tar.gz", hash = "sha256:9b272268794172b0b8bb9fb1f3c470c3b6c0ffb92fbd4882465cc740e40fbdcd", size = 111358 }
sdist = { url = "https://files.pythonhosted.org/packages/9f/91/899a795437da14cbf401b026b6df57fa37973a789052eb6c1d9c5e8cf456/boto3-1.37.20.tar.gz", hash = "sha256:87d9bd6ad49be754d4ae2724cfb892eb3f9f17bcafd781fb3ce0d98cc539bdd6", size = 111372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/94/dccc4dd874cf455c8ea6dfb4c43a224632c03c3f503438aa99021759a097/boto3-1.37.18-py3-none-any.whl", hash = "sha256:1545c943f36db41853cdfdb6ff09c4eda9220dd95bd2fae76fc73091603525d1", size = 139561 },
{ url = "https://files.pythonhosted.org/packages/6e/98/bac2404ff6183e1aaeebfefe6f345d63a1395b9a710be5ad24dcad9538ed/boto3-1.37.20-py3-none-any.whl", hash = "sha256:225dbc75d79816cb9b28cc74a63c9fa0f2d70530d603dacd82634f362f6679c1", size = 139561 },
]
[[package]]
name = "botocore"
version = "1.37.18"
version = "1.37.20"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2c/fa/a176046c74032ca3bda68c71ad544602a69be21d7ee3b199f4f2099fe4bf/botocore-1.37.18.tar.gz", hash = "sha256:99e8eefd5df6347ead15df07ce55f4e62a51ea7b54de1127522a08597923b726", size = 13667977 }
sdist = { url = "https://files.pythonhosted.org/packages/5d/eb/b90ec01a82278a283db0b788f0a36201e485ceb31df762c44cddbcda2085/botocore-1.37.20.tar.gz", hash = "sha256:9295385740f9d30f9b679f76ee51f49b80ae73183d84d499c1c3f1d54d820f54", size = 13670736 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/fd/059e57de7405b1ba93117c1b79a6daa45d6865f557b892f3cc7645836f3b/botocore-1.37.18-py3-none-any.whl", hash = "sha256:a8b97d217d82b3c4f6bcc906e264df7ebb51e2c6a62b3548a97cd173fb8759a1", size = 13428387 },
{ url = "https://files.pythonhosted.org/packages/2d/5b/f96cf58c37704b907ac2f9cc94e45ba0a2aa3b2062421aa8b8614f1d78de/botocore-1.37.20-py3-none-any.whl", hash = "sha256:c34f4f25fda7c4f726adf5a948590bd6bd7892c05278d31e344b5908e7b43301", size = 13432464 },
]
[[package]]
@ -318,14 +318,14 @@ wheels = [
[[package]]
name = "griffe"
version = "1.6.2"
version = "1.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2f/f2/b00eb72b853ecb5bf31dd47857cdf6767e380ca24ec2910d43b3fa7cc500/griffe-1.6.2.tar.gz", hash = "sha256:3a46fa7bd83280909b63c12b9a975732a927dd97809efe5b7972290b606c5d91", size = 392836 }
sdist = { url = "https://files.pythonhosted.org/packages/bf/28/f61564ce1237727f0daa0b28e18201252e032aa7d7fb0e4fa0b447ecad4b/griffe-1.6.3.tar.gz", hash = "sha256:568cc9e50de04f6c76234bf46dd7f3a264ea3cbb1380fb54818e81e3675a83cf", size = 393810 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/bc/bd8b7de5e748e078b6be648e76b47189a9182b1ac1eb7791ff7969f39f27/griffe-1.6.2-py3-none-any.whl", hash = "sha256:6399f7e663150e4278a312a8e8a14d2f3d7bd86e2ef2f8056a1058e38579c2ee", size = 128638 },
{ url = "https://files.pythonhosted.org/packages/8a/11/46f2e6370ecf8e9e3ab01ad422fb9ea5b90579bdc4b1a8527aa745b20758/griffe-1.6.3-py3-none-any.whl", hash = "sha256:7a0c559f10d8a9016f4d0b4ceaacc087e31e2370cb1aa9a59006a30d5a279fb3", size = 128922 },
]
[[package]]
@ -488,7 +488,7 @@ wheels = [
[[package]]
name = "llm-review"
version = "0.3.0"
version = "0.4.0"
source = { virtual = "." }
dependencies = [
{ name = "playwright" },
@ -503,11 +503,11 @@ requires-dist = [
[[package]]
name = "logfire-api"
version = "3.9.0"
version = "3.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/ff/0fda08241cc005a7afad3901939b43129869d43640e8f4fb35eac7fd9443/logfire_api-3.9.0.tar.gz", hash = "sha256:b03bdcf368595510b4417270b5f02b268eb571a25692248ac1894b841a983a90", size = 47152 }
sdist = { url = "https://files.pythonhosted.org/packages/31/9d/c459af8629bae9f1fc2b02fd51975f9ab69daa02edb369781081c3b5e8f5/logfire_api-3.10.0.tar.gz", hash = "sha256:08ef320a1df24593332f78dafdb82f914af73c5b332bab207447f7fee8c0cc3a", size = 47224 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/c3/b05f4ddfc90babaef730e7ddd3b420d024f6ad77a8d1883546eec3b25f9a/logfire_api-3.9.0-py3-none-any.whl", hash = "sha256:a313eba49976ccca62ba6acb2f454d28941e53a114b73a29c50e8c09ea38767d", size = 77962 },
{ url = "https://files.pythonhosted.org/packages/c7/9b/a1575589f732bbb3b8d21cfa3709ba8d2f5c278a2cb2650c44029dd8a03f/logfire_api-3.10.0-py3-none-any.whl", hash = "sha256:eb62d572eab349e3746978b3ccb8decbeefd24b35187a69779cb7519aa73df22", size = 78067 },
]
[[package]]
@ -674,19 +674,19 @@ wheels = [
[[package]]
name = "pydantic-ai"
version = "0.0.43"
version = "0.0.46"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic-ai-slim", extra = ["anthropic", "bedrock", "cli", "cohere", "groq", "mcp", "mistral", "openai", "vertexai"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/39/6e698f4794dc9af983ccb7f5d2e4bf3dbd887f75cc9d013a50f5d0512a69/pydantic_ai-0.0.43.tar.gz", hash = "sha256:7fb144eeee16abda0956befbee2577071a77a403c3fd7bb4a71562765812e072", size = 11756814 }
sdist = { url = "https://files.pythonhosted.org/packages/ca/11/12ce8ce4ceec38f962b18970d9d4fa426328fadf688e205aed9b02e10288/pydantic_ai-0.0.46.tar.gz", hash = "sha256:170827b35808f126070898f4169b59960a018e8643b0332369c7050665723b66", size = 11758925 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/24/5d1d2eea5bee7977961831abb220cf4bd59a2d6f883290824c71256ddef7/pydantic_ai-0.0.43-py3-none-any.whl", hash = "sha256:82dd077771072edcf1d50db3d8a83941456541e272b2bb7e0cb411d1d7533734", size = 9740 },
{ url = "https://files.pythonhosted.org/packages/0e/de/287e27a1e4657f4f24e4a33ff03f9bc73deea6f630830507b248e02de07e/pydantic_ai-0.0.46-py3-none-any.whl", hash = "sha256:777688d120494d4844e9b79a8bb813cadd0b6bb4fc71887a00bfca3e050a5d68", size = 9741 },
]
[[package]]
name = "pydantic-ai-slim"
version = "0.0.43"
version = "0.0.46"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "eval-type-backport" },
@ -697,9 +697,9 @@ dependencies = [
{ name = "pydantic-graph" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d3/f8/45fa567d4bcd309bf5f08390443f84893dca74cf3ecebcd743f7df1ba9c7/pydantic_ai_slim-0.0.43.tar.gz", hash = "sha256:4e31b079b7a4fd335b775ab07779831b737bc545b130748a18c33bdf08b76424", size = 106556 }
sdist = { url = "https://files.pythonhosted.org/packages/23/6e/06154eac4c7b8f960f6a8daef9c907b30fbfc3ae66d73d4731d3c243f7ae/pydantic_ai_slim-0.0.46.tar.gz", hash = "sha256:235486e5116fa22ccd7363fee9e30b0acc3735525991e4aa76b8647cb180d6b0", size = 104145 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/ea/efb0efa1ac0d8254041cab2ed4a72478d68f9311d95f2505de41bc0c2fa7/pydantic_ai_slim-0.0.43-py3-none-any.whl", hash = "sha256:b3bbf353827dc8b38a53979dacdf31d650366204408c574238dd168821b9b9cd", size = 140618 },
{ url = "https://files.pythonhosted.org/packages/0b/74/26b2f6eda0d1ebe34229d3813b484968b5edd388b039ccb1b0edaaa45975/pydantic_ai_slim-0.0.46-py3-none-any.whl", hash = "sha256:7318f93583ac36f03263c6093343c94506dad753a9b0ee19a0074d83e898dd49", size = 137454 },
]
[package.optional-dependencies]
@ -789,7 +789,7 @@ wheels = [
[[package]]
name = "pydantic-graph"
version = "0.0.43"
version = "0.0.46"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@ -797,9 +797,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/12/50/b7b11e0d75c2f958be8688fde138c413f3c29136e1934602c14c7581fd5c/pydantic_graph-0.0.43.tar.gz", hash = "sha256:648e70c7978e9dee0b79694aa43f57be330c98fd69edf33674e0515fe709ae43", size = 20316 }
sdist = { url = "https://files.pythonhosted.org/packages/d2/38/ebbbe87ddd813ebeb671f1c8d3b0b79d183be40c9e0d517a4361a7d9cf51/pydantic_graph-0.0.46.tar.gz", hash = "sha256:3a0afc4915102083b81879af3c41100b0baddb6cda0b51f919621a061622c4e4", size = 20321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/1f/fb319fcce952f1956a1a27cb66d71dc5c7ed970804a105e766be7e28907e/pydantic_graph-0.0.43-py3-none-any.whl", hash = "sha256:d1670ec8457367cc21452e90beeacf951ccc8ec07baa6f66f1bb56fe1823d472", size = 25807 },
{ url = "https://files.pythonhosted.org/packages/8a/a6/118286e8ab29ab9e2fa16e02792903a993e391cdfd1029bf7e6f6d8feed3/pydantic_graph-0.0.46-py3-none-any.whl", hash = "sha256:df74a593cea2597569b8f4f5adc4af713cf90d815f6e9cc1616e0be2dfa3e1f3", size = 25808 },
]
[[package]]
@ -850,11 +850,11 @@ wheels = [
[[package]]
name = "python-dotenv"
version = "1.0.1"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
]
[[package]]
@ -1038,11 +1038,11 @@ wheels = [
[[package]]
name = "typing-extensions"
version = "4.12.2"
version = "4.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
{ url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 },
]
[[package]]