Skip to content

Enforce mypy and fix typing issues #145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

loganrosen
Copy link
Contributor

I noticed that there are mypy issues in my editor since it isn't being enforced, so I fixed the error and added enforcement for it.

I tried to avoid using cast as much as possible, which sometimes required creating multiple variables where there once was one (mypy doesn't like when variables are reassigned with a different type). I also added TypeGuards in certain cases where appropriate.

The langgraph dependency in uv.lock was also updated to latest to pull in this fix to its typing.

Copy link
Collaborator

@vbarda vbarda left a comment

Choose a reason for hiding this comment

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

thanks for adding! let's roll back a bunch of unnecessary naming changes, otherwise looks good to me

you would also need to run make format on the codebase

@@ -11,7 +11,7 @@
AgentNameMode = Literal["inline"]


def _is_content_blocks_content(content: list[dict] | str) -> bool:
def _is_content_blocks_content(content: list[dict | str] | str) -> TypeGuard[list[dict]]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

i don't think there should be a list of str here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was needed because of how BaseMessage.content is typed.

@@ -35,12 +35,13 @@ def add_inline_agent_name(message: BaseMessage) -> BaseMessage:
return message

formatted_message = message.model_copy()
if _is_content_blocks_content(formatted_message.content):
if _is_content_blocks_content(message.content):
Copy link
Collaborator

Choose a reason for hiding this comment

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

why change this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This allows the new TypeGuard returned by _is_content_blocks_content to treat message.content as list[dict]. message.content is used/iterated within this conditional, so it requires the type guard to be treated as a list[dict].

text_blocks = [block for block in message.content if block["type"] == "text"]
non_text_blocks = [block for block in message.content if block["type"] != "text"]
content = text_blocks[0]["text"] if text_blocks else ""
formatted_content = f"<name>{message.name}</name><content>{content}</content>"
formatted_message.content = [{"type": "text", "text": formatted_content}] + non_text_blocks
formatted_message_content = [{"type": "text", "text": formatted_content}] + non_text_blocks
Copy link
Collaborator

Choose a reason for hiding this comment

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

why change this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a weird one. Storing it in an intermediate variable avoids this mypy error from assigning the expression directly to formatted_message.content:

Incompatible types in assignment (expression has type "list[dict[str, str]]", variable has type "str | list[str | dict[Any, Any]]")

I think the only alternative would be to explicitly cast to the expected type (which I prefer to avoid if possible).

if is_content_blocks_content:
text_blocks = [block for block in message.content if block["type"] == "text"]
if is_content_blocks_content := _is_content_blocks_content(message.content):
content_blocks_content = message.content
Copy link
Collaborator

Choose a reason for hiding this comment

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

why change this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought it was necessary to get around a mypy error, but it looks like it's not. Removed.

@@ -18,6 +18,11 @@ def _normalize_agent_name(agent_name: str) -> str:
return WHITESPACE_RE.sub("_", agent_name.strip()).lower()


def _has_multiple_content_blocks(content: str | list[str | dict]) -> TypeGuard[list[dict]]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

don't think it should have list of str

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above – it's due to the type of BaseMessage.content.

handoff_destinations = _get_handoff_destinations(tools or [])
if handoff_destinations:
if missing_handoff_destinations := set(agent_names) - set(handoff_destinations):
extracted_handoff_destinations = _get_handoff_destinations(
Copy link
Collaborator

Choose a reason for hiding this comment

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

why change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_get_handoff_destinations returns a list[str], but then down in the else block, handoff_destinations becomes a list[BaseTool]. This change is to avoid having to cast the handoff_destinations variable from list[str] to list[BaseTool] later on.

def test_add_inline_agent_name_content_blocks():
content_blocks = [
def test_add_inline_agent_name_content_blocks() -> None:
content_blocks: list[str | dict] = [
Copy link
Collaborator

Choose a reason for hiding this comment

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

this should be just list of dict

Copy link
Contributor Author

Choose a reason for hiding this comment

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

content_blocks is passed as the content parameter to AIMessage below, and that expects list[str | dict]. Because list is invariant, mypy won't accept a list[str] passed to a list[str | dict].

Perhaps it would be better for AIMessage to expect list[str] | list[dict] (or even Sequence[str | dict], since Sequence is covariant) to avoid needing to do this, but that would require a change in that library. Happy to do that if you think that's preferable.

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.

2 participants