Repository: JoshuaC215/agent-service-toolkit
Pull request #205: Fixing mypy
Author: login: maver1ck
Created: 2025-04-15T13:02:05Z | Merged: 2025-04-21T16:56:19Z

Fixes #199 

Done:
- all mypy errors with exception for changes from https://github.com/JoshuaC215/agent-service-toolkit/pull/202
- ignore of streamlit_app.py
- adding mypy to Github action

Result:
```
❯ mypy src  
src/memory/postgres.py:37: error: Item "None" of "SecretStr | None" has no attribute "get_secret_value"  [union-attr]
src/memory/postgres.py:71: error: Argument "conn" to "AsyncPostgresSaver" has incompatible type "AsyncConnectionPool[AsyncConnection[tuple[Any, ...]]]"; expected "AsyncConnection[dict[str, Any]] | AsyncConnectionPool[AsyncConnection[dict[str, Any]]]"  [arg-type]
src/memory/postgres.py:72: error: Incompatible return value type (got "AsyncPostgresSaver", expected "AbstractAsyncContextManager[AsyncPostgresSaver, bool | None]")  [return-value]
Found 3 errors in 1 file (checked 30 source files)
```

Linked issue #199: Support for Mypy
Hi @JoshuaC215,
I really appreciate your work. And I'd like to ask if you consider supporting mypy in the repo ?

Currently it's giving some error:
```
$ mypy .
...
Found 88 errors in 17 files (checked 31 source files)
```
I may help with fixing them :)

What do you think ?

Regards,
Maciek
Linked issue comments: user: login: JoshuaC215
created_at: 2025-04-09T04:12:46Z
body: Hey @maver1ck, cheers, thanks for taking a look.

Yeah, I'm wondering how invasive the changes are but in principle I'm open to it. In particular, if it has a bad time with Langchain / Langgraph and requires a lot of extra boilerplate or something, I might want to live without it.

If it's not too bad and we want to add it, would also be nice to update the pre-commit and CI setup to check it going forward.
author_association: OWNER
user: login: maver1ck
created_at: 2025-04-10T07:01:08Z
body: Hi @JoshuaC215 
Can we start with some upgrades of packages / uv.lock ? Probably as a separate PR ?
EDIT: https://github.com/JoshuaC215/agent-service-toolkit/pull/200

For instance support for mypy in langgraph-supervisor is 3 weeks old:
Reference: 
https://github.com/langchain-ai/langgraph-supervisor-py/tree/main/langgraph_supervisor

Mypy run.
Before upgrade:
```
Found 94 errors in 18 files (checked 31 source files)
```
After:
```
Found 88 errors in 17 files (checked 31 source files)
```
author_association: CONTRIBUTOR
user: login: maver1ck
created_at: 2025-04-10T10:59:18Z
body: I will try do some fixes module after module.
Some problems may be non-trivial to fix because of mixed usage of AnyMessage and BaseMessage within Langchain and Langgraph. First PR created.
author_association: CONTRIBUTOR
user: login: maver1ck
created_at: 2025-04-15T18:13:27Z
body: @JoshuaC215 PR is ready to review
author_association: CONTRIBUTOR

Diff:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 919aae17..08acb4d6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -37,6 +37,10 @@ jobs:
         uv run ruff format --check
         uv run ruff check --output-format github
 
+    - name: Run mypy
+      run: |
+        uv run mypy src/
+
     - name: Test with pytest
       run: |
         uv run pytest --cov=src/ --cov-report=xml
diff --git a/pyproject.toml b/pyproject.toml
index 16600755..b4dc22c9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -58,6 +58,7 @@ dev = [
     "pytest-env",
     "pytest-asyncio",
     "ruff",
+    "mypy",
 ]
 
 # Group for the minimal dependencies to run just the client and Streamlit app.
@@ -83,3 +84,11 @@ asyncio_default_fixture_loop_scope = "function"
 
 [tool.pytest_env]
 OPENAI_API_KEY = "sk-fake-openai-key"
+
+[tool.mypy]
+plugins = "pydantic.mypy"
+exclude = "src/streamlit_app.py"
+
+[[tool.mypy.overrides]]
+module = ["numexpr.*"]
+follow_untyped_imports = true
diff --git a/src/agents/agents.py b/src/agents/agents.py
index 5918f1e3..307d838f 100644
--- a/src/agents/agents.py
+++ b/src/agents/agents.py
@@ -1,6 +1,6 @@
 from dataclasses import dataclass
 
-from langgraph.graph.state import CompiledStateGraph
+from langgraph.pregel import Pregel
 
 from agents.bg_task_agent.bg_task_agent import bg_task_agent
 from agents.chatbot import chatbot
@@ -16,7 +16,7 @@
 @dataclass
 class Agent:
     description: str
-    graph: CompiledStateGraph
+    graph: Pregel
 
 
 agents: dict[str, Agent] = {
@@ -33,7 +33,7 @@ class Agent:
 }
 
 
-def get_agent(agent_id: str) -> CompiledStateGraph:
+def get_agent(agent_id: str) -> Pregel:
     return agents[agent_id].graph
 
 
diff --git a/src/agents/bg_task_agent/bg_task_agent.py b/src/agents/bg_task_agent/bg_task_agent.py
index fe59c9a3..6948313d 100644
--- a/src/agents/bg_task_agent/bg_task_agent.py
+++ b/src/agents/bg_task_agent/bg_task_agent.py
@@ -23,7 +23,7 @@ def wrap_model(model: BaseChatModel) -> RunnableSerializable[AgentState, AIMessa
         lambda state: state["messages"],
         name="StateModifier",
     )
-    return preprocessor | model
+    return preprocessor | model  # type: ignore[return-value]
 
 
 async def acall_model(state: AgentState, config: RunnableConfig) -> AgentState:
diff --git a/src/agents/bg_task_agent/task.py b/src/agents/bg_task_agent/task.py
index a3efa588..d42019e8 100644
--- a/src/agents/bg_task_agent/task.py
+++ b/src/agents/bg_task_agent/task.py
@@ -16,7 +16,7 @@ def __init__(self, task_name: str, writer: StreamWriter | None = None) -> None:
         self.result: Literal["success", "error"] | None = None
         self.writer = writer
 
-    def _generate_and_dispatch_message(self, writer: StreamWriter, data: dict):
+    def _generate_and_dispatch_message(self, writer: StreamWriter | None, data: dict):
         writer = writer or self.writer
         task_data = TaskData(name=self.name, run_id=self.id, state=self.state, data=data)
         if self.result:
@@ -25,7 +25,8 @@ def _generate_and_dispatch_message(self, writer: StreamWriter, data: dict):
             type=self.name,
             data=task_data.model_dump(),
         )
-        task_custom_data.dispatch(writer)
+        if writer:
+            task_custom_data.dispatch(writer)
         return task_custom_data.to_langchain()
 
     def start(self, writer: StreamWriter | None = None, data: dict = {}) -> BaseMessage:
diff --git a/src/agents/chatbot.py b/src/agents/chatbot.py
index 394c8b14..2eca6761 100644
--- a/src/agents/chatbot.py
+++ b/src/agents/chatbot.py
@@ -2,7 +2,6 @@
 from langchain_core.runnables import RunnableConfig
 from langgraph.checkpoint.memory import MemorySaver
 from langgraph.func import entrypoint
-from langgraph.graph import add_messages
 
 from core import get_model, settings
 
@@ -16,10 +15,10 @@ async def chatbot(
 ):
     messages = inputs["messages"]
     if previous:
-        messages = add_messages(previous["messages"], messages)
+        messages = previous["messages"] + messages
 
     model = get_model(config["configurable"].get("model", settings.DEFAULT_MODEL))
     response = await model.ainvoke(messages)
     return entrypoint.final(
-        value={"messages": [response]}, save={"messages": add_messages(messages, response)}
+        value={"messages": [response]}, save={"messages": messages + [response]}
     )
diff --git a/src/agents/interrupt_agent.py b/src/agents/interrupt_agent.py
index c0c44979..a9b83b06 100644
--- a/src/agents/interrupt_agent.py
+++ b/src/agents/interrupt_agent.py
@@ -1,10 +1,11 @@
 from datetime import datetime
-from typing import cast
+from typing import Any
 
 from langchain.prompts import SystemMessagePromptTemplate
+from langchain_core.language_models.base import LanguageModelInput
 from langchain_core.language_models.chat_models import BaseChatModel
-from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
-from langchain_core.runnables import RunnableConfig, RunnableLambda, RunnableSerializable
+from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
+from langchain_core.runnables import Runnable, RunnableConfig, RunnableLambda, RunnableSerializable
 from langgraph.checkpoint.memory import MemorySaver
 from langgraph.graph import END, MessagesState, StateGraph
 from langgraph.types import interrupt
@@ -23,8 +24,8 @@ class AgentState(MessagesState, total=False):
 
 
 def wrap_model(
-    model: BaseChatModel, system_prompt: SystemMessage
-) -> RunnableSerializable[AgentState, AIMessage]:
+    model: BaseChatModel | Runnable[LanguageModelInput, Any], system_prompt: BaseMessage
+) -> RunnableSerializable[AgentState, Any]:
     preprocessor = RunnableLambda(
         lambda state: [system_prompt] + state["messages"],
         name="StateModifier",
@@ -75,8 +76,7 @@ async def determine_birthdate(state: AgentState, config: RunnableConfig) -> Agen
     model_runnable = wrap_model(
         m.with_structured_output(BirthdateExtraction), birthdate_extraction_prompt.format()
     ).with_config(tags=["skip_stream"])
-    response = await model_runnable.ainvoke(state, config)
-    response = cast(BirthdateExtraction, response)
+    response: BirthdateExtraction = await model_runnable.ainvoke(state, config)
 
     # If no birthdate found, interrupt
     if response.birthdate is None:
@@ -98,6 +98,7 @@ async def determine_birthdate(state: AgentState, config: RunnableConfig) -> Agen
 
     # Birthdate found
     return {
+        "messages": [],
         "birthdate": birthdate,
     }
 
@@ -115,7 +116,8 @@ async def determine_sign(state: AgentState, config: RunnableConfig) -> AgentStat
 
     m = get_model(config["configurable"].get("model", settings.DEFAULT_MODEL))
     model_runnable = wrap_model(
-        m, sign_prompt.format(birthdate=state["birthdate"].strftime("%Y-%m-%d"))
+        m,
+        sign_prompt.format(birthdate=state["birthdate"].strftime("%Y-%m-%d")),  # type: ignore[union-attr]
     )
     response = await model_runnable.ainvoke(state, config)
 
diff --git a/src/agents/llama_guard.py b/src/agents/llama_guard.py
index 96c96041..eb18679c 100644
--- a/src/agents/llama_guard.py
+++ b/src/agents/llama_guard.py
@@ -97,14 +97,14 @@ def invoke(self, role: str, messages: list[AnyMessage]) -> LlamaGuardOutput:
             return LlamaGuardOutput(safety_assessment=SafetyAssessment.SAFE)
         compiled_prompt = self._compile_prompt(role, messages)
         result = self.model.invoke([HumanMessage(content=compiled_prompt)])
-        return parse_llama_guard_output(result.content)
+        return parse_llama_guard_output(str(result.content))
 
     async def ainvoke(self, role: str, messages: list[AnyMessage]) -> LlamaGuardOutput:
         if self.model is None:
             return LlamaGuardOutput(safety_assessment=SafetyAssessment.SAFE)
         compiled_prompt = self._compile_prompt(role, messages)
         result = await self.model.ainvoke([HumanMessage(content=compiled_prompt)])
-        return parse_llama_guard_output(result.content)
+        return parse_llama_guard_output(str(result.content))
 
 
 if __name__ == "__main__":
diff --git a/src/agents/research_assistant.py b/src/agents/research_assistant.py
index cdcc9afa..f0700602 100644
--- a/src/agents/research_assistant.py
+++ b/src/agents/research_assistant.py
@@ -53,12 +53,12 @@ class AgentState(MessagesState, total=False):
 
 
 def wrap_model(model: BaseChatModel) -> RunnableSerializable[AgentState, AIMessage]:
-    model = model.bind_tools(tools)
+    bound_model = model.bind_tools(tools)
     preprocessor = RunnableLambda(
         lambda state: [SystemMessage(content=instructions)] + state["messages"],
         name="StateModifier",
     )
-    return preprocessor | model
+    return preprocessor | bound_model  # type: ignore[return-value]
 
 
 def format_safety_message(safety: LlamaGuardOutput) -> AIMessage:
@@ -95,7 +95,7 @@ async def acall_model(state: AgentState, config: RunnableConfig) -> AgentState:
 async def llama_guard_input(state: AgentState, config: RunnableConfig) -> AgentState:
     llama_guard = LlamaGuard()
     safety_output = await llama_guard.ainvoke("User", state["messages"])
-    return {"safety": safety_output}
+    return {"safety": safety_output, "messages": []}
 
 
 async def block_unsafe_content(state: AgentState, config: RunnableConfig) -> AgentState:
diff --git a/src/client/client.py b/src/client/client.py
index b874e90f..a9a4b0a0 100644
--- a/src/client/client.py
+++ b/src/client/client.py
@@ -26,7 +26,7 @@ class AgentClient:
     def __init__(
         self,
         base_url: str = "http://0.0.0.0",
-        agent: str = None,
+        agent: str | None = None,
         timeout: float | None = None,
         get_info: bool = True,
     ) -> None:
@@ -68,7 +68,7 @@ def retrieve_info(self) -> None:
         except httpx.HTTPError as e:
             raise AgentClientError(f"Error getting service info: {e}")
 
-        self.info: ServiceMetadata = ServiceMetadata.model_validate(response.json())
+        self.info = ServiceMetadata.model_validate(response.json())
         if not self.agent or self.agent not in [a.key for a in self.info.agents]:
             self.agent = self.info.default_agent
 
@@ -76,7 +76,7 @@ def update_agent(self, agent: str, verify: bool = True) -> None:
         if verify:
             if not self.info:
                 self.retrieve_info()
-            agent_keys = [a.key for a in self.info.agents]
+            agent_keys = [a.key for a in self.info.agents]  # type: ignore[union-attr]
             if agent not in agent_keys:
                 raise AgentClientError(
                     f"Agent {agent} not found in available agents: {', '.join(agent_keys)}"
@@ -108,7 +108,7 @@ async def ainvoke(
         if thread_id:
             request.thread_id = thread_id
         if model:
-            request.model = model
+            request.model = model  # type: ignore[assignment]
         if agent_config:
             request.agent_config = agent_config
         async with httpx.AsyncClient() as client:
@@ -150,7 +150,7 @@ def invoke(
         if thread_id:
             request.thread_id = thread_id
         if model:
-            request.model = model
+            request.model = model  # type: ignore[assignment]
         if agent_config:
             request.agent_config = agent_config
         try:
@@ -223,7 +223,7 @@ def stream(
         if thread_id:
             request.thread_id = thread_id
         if model:
-            request.model = model
+            request.model = model  # type: ignore[assignment]
         if agent_config:
             request.agent_config = agent_config
         try:
@@ -276,7 +276,7 @@ async def astream(
         if thread_id:
             request.thread_id = thread_id
         if model:
-            request.model = model
+            request.model = model  # type: ignore[assignment]
         if agent_config:
             request.agent_config = agent_config
         async with httpx.AsyncClient() as client:
diff --git a/src/core/llm.py b/src/core/llm.py
index fb733f3e..888abd35 100644
--- a/src/core/llm.py
+++ b/src/core/llm.py
@@ -47,10 +47,6 @@
     FakeModelName.FAKE: "fake",
 }
 
-ModelT: TypeAlias = (
-    ChatOpenAI | ChatAnthropic | ChatGoogleGenerativeAI | ChatGroq | ChatBedrock | ChatOllama
-)
-
 
 class FakeToolModel(FakeListChatModel):
     def __init__(self, responses: list[str]):
@@ -60,6 +56,18 @@ def bind_tools(self, tools):
         return self
 
 
+ModelT: TypeAlias = (
+    AzureChatOpenAI
+    | ChatOpenAI
+    | ChatAnthropic
+    | ChatGoogleGenerativeAI
+    | ChatGroq
+    | ChatBedrock
+    | ChatOllama
+    | FakeToolModel
+)
+
+
 @cache
 def get_model(model_name: AllModelEnum, /) -> ModelT:
     # NOTE: models with streaming=True will send tokens as they are generated
@@ -122,3 +130,5 @@ def get_model(model_name: AllModelEnum, /) -> ModelT:
         return chat_ollama
     if model_name in FakeModelName:
         return FakeToolModel(responses=["This is a test response from the fake model."])
+
+    raise ValueError(f"Unsupported model: {model_name}")
diff --git a/src/core/settings.py b/src/core/settings.py
index b319af03..59360401 100644
--- a/src/core/settings.py
+++ b/src/core/settings.py
@@ -187,7 +187,7 @@ def model_post_init(self, __context: Any) -> None:
                 case _:
                     raise ValueError(f"Unknown provider: {provider}")
 
-    @computed_field
+    @computed_field  # type: ignore[prop-decorator]
     @property
     def BASE_URL(self) -> str:
         return f"http://{self.HOST}:{self.PORT}"
diff --git a/src/schema/schema.py b/src/schema/schema.py
index 1b2fbfee..d6a12488 100644
--- a/src/schema/schema.py
+++ b/src/schema/schema.py
@@ -131,7 +131,7 @@ def pretty_print(self) -> None:
         print(self.pretty_repr())  # noqa: T201
 
 
-class Feedback(BaseModel):
+class Feedback(BaseModel):  # type: ignore[no-redef]
     """Feedback for a run, to record to LangSmith."""
 
     run_id: str = Field(
diff --git a/src/schema/task_data.py b/src/schema/task_data.py
index 818cf818..49a20d85 100644
--- a/src/schema/task_data.py
+++ b/src/schema/task_data.py
@@ -71,4 +71,4 @@ def add_and_draw_task_data(self, task_data: TaskData) -> None:
         # Status is "running" until all tasks have completed
         else:
             state = "running"
-        status.update(state=state)
+        status.update(state=state)  # type: ignore[arg-type]
diff --git a/src/service/service.py b/src/service/service.py
index 1d4b6acf..24ab976a 100644
--- a/src/service/service.py
+++ b/src/service/service.py
@@ -12,7 +12,7 @@
 from langchain_core._api import LangChainBetaWarning
 from langchain_core.messages import AIMessage, AIMessageChunk, AnyMessage, HumanMessage, ToolMessage
 from langchain_core.runnables import RunnableConfig
-from langgraph.graph.state import CompiledStateGraph
+from langgraph.pregel import Pregel
 from langgraph.types import Command, Interrupt
 from langsmith import Client as LangsmithClient
 
@@ -86,9 +86,7 @@ async def info() -> ServiceMetadata:
     )
 
 
-async def _handle_input(
-    user_input: UserInput, agent: CompiledStateGraph
-) -> tuple[dict[str, Any], UUID]:
+async def _handle_input(user_input: UserInput, agent: Pregel) -> tuple[dict[str, Any], UUID]:
     """
     Parse user input and handle any required interrupt resumption.
     Returns kwargs for agent invocation and the run_id.
@@ -117,6 +115,7 @@ async def _handle_input(
         task for task in state.tasks if hasattr(task, "interrupts") and task.interrupts
     ]
 
+    input: Command | dict[str, Any]
     if interrupted_tasks:
         # assume user input is response to resume agent execution from interrupt
         input = Command(resume=user_input.message)
@@ -146,10 +145,10 @@ async def invoke(user_input: UserInput, agent_id: str = DEFAULT_AGENT) -> ChatMe
     # in interrupt-agent, or a tool step in research-assistant), it's omitted. Arguably,
     # you'd want to include it. You could update the API to return a list of ChatMessages
     # in that case.
-    agent: CompiledStateGraph = get_agent(agent_id)
+    agent: Pregel = get_agent(agent_id)
     kwargs, run_id = await _handle_input(user_input, agent)
     try:
-        response_events = await agent.ainvoke(**kwargs, stream_mode=["updates", "values"])
+        response_events: list[tuple[str, Any]] = await agent.ainvoke(**kwargs, stream_mode=["updates", "values"])  # type: ignore # fmt: skip
         response_type, response = response_events[-1]
         if response_type == "values":
             # Normal response, the agent completed successfully
@@ -178,7 +177,7 @@ async def message_generator(
 
     This is the workhorse method for the /stream endpoint.
     """
-    agent: CompiledStateGraph = get_agent(agent_id)
+    agent: Pregel = get_agent(agent_id)
     kwargs, run_id = await _handle_input(user_input, agent)
 
     try:
@@ -258,7 +257,7 @@ async def message_generator(
         yield "data: [DONE]\n\n"
 
 
-def _sse_response_example() -> dict[int, Any]:
+def _sse_response_example() -> dict[int | str, Any]:
     return {
         status.HTTP_200_OK: {
             "description": "Server Sent Event Response",
@@ -320,7 +319,7 @@ def history(input: ChatHistoryInput) -> ChatHistory:
     Get chat history.
     """
     # TODO: Hard-coding DEFAULT_AGENT here is wonky
-    agent: CompiledStateGraph = get_agent(DEFAULT_AGENT)
+    agent: Pregel = get_agent(DEFAULT_AGENT)
     try:
         state_snapshot = agent.get_state(
             config=RunnableConfig(
diff --git a/uv.lock b/uv.lock
index a616cec0..fe7386a7 100644
--- a/uv.lock
+++ b/uv.lock
@@ -55,6 +55,7 @@ client = [
     { name = "streamlit" },
 ]
 dev = [
+    { name = "mypy" },
     { name = "pre-commit" },
     { name = "pytest" },
     { name = "pytest-asyncio" },
@@ -107,6 +108,7 @@ client = [
     { name = "streamlit", specifier = "~=1.40.1" },
 ]
 dev = [
+    { name = "mypy" },
     { name = "pre-commit" },
     { name = "pytest" },
     { name = "pytest-asyncio" },
@@ -1482,6 +1484,37 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400 },
 ]
 
+[[package]]
+name = "mypy"
+version = "1.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mypy-extensions" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 },
+    { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 },
+    { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 },
+    { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751 },
+    { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783 },
+    { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618 },
+    { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 },
+    { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 },
+    { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 },
+    { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 },
+    { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 },
+    { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 },
+    { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
+    { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
+    { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
+    { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
+    { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
+    { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
+    { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
+]
+
 [[package]]
 name = "mypy-extensions"
 version = "1.0.0"


Reviews:
user: login: JoshuaC215
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-21T00:28:05Z
user: login: maver1ck
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T15:43:00Z
user: login: JoshuaC215
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-21T16:56:09Z

Review comments:
diff_hunk: @@ -16,10 +15,10 @@ async def chatbot(
 ):
     messages = inputs["messages"]
     if previous:
-        messages = add_messages(previous["messages"], messages)
+        messages = previous["messages"] + messages
path: src/agents/chatbot.py
user: login: JoshuaC215
body: This (and below) slightly changes the behavior and deviates from the langgraph best practice as I understand it - is it needed?

Function definition is here, for reference: https://github.com/langchain-ai/langgraph/blob/main/libs/langgraph/langgraph/graph/message.py
created_at: 2025-04-21T00:28:05Z
author_association: OWNER
diff_hunk: @@ -16,10 +15,10 @@ async def chatbot(
 ):
     messages = inputs["messages"]
     if previous:
-        messages = add_messages(previous["messages"], messages)
+        messages = previous["messages"] + messages
path: src/agents/chatbot.py
user: login: maver1ck
body: This change fixes those errors which I found super difficult to fix any other way.
```
src/agents/chatbot.py:19: error: Incompatible types in assignment (expression has type "list[BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]] | BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]", variable has type "list[BaseMessage]")  [assignment]
src/agents/chatbot.py:19: error: Argument 1 to "add_messages" has incompatible type "list[BaseMessage]"; expected "list[BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]] | BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]"  [arg-type]
src/agents/chatbot.py:19: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
src/agents/chatbot.py:19: note: Consider using "Sequence" instead, which is covariant
src/agents/chatbot.py:19: error: Argument 2 to "add_messages" has incompatible type "list[BaseMessage]"; expected "list[BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]] | BaseMessage | list[str] | tuple[str, str] | str | dict[str, Any]"  [arg-type]
```
created_at: 2025-04-21T15:43:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-04-15T16:24:42Z
body: ## [Codecov](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) Report
Attention: Patch coverage is `61.11111%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 67.47%. Comparing base [(`37c3200`)](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/commit/37c32004e8c6dfea30adef695ed645d3937e447e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) to head [(`a06f97b`)](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/commit/a06f97b26613a1bddbbe1a6386b4576df5d26a67?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) | Patch % | Lines |
|---|---|---|
| [src/agents/research\_assistant.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Fresearch_assistant.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9yZXNlYXJjaF9hc3Npc3RhbnQucHk=) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) |
| [src/agents/bg\_task\_agent/task.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Fbg_task_agent%2Ftask.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9iZ190YXNrX2FnZW50L3Rhc2sucHk=) | 33.33% | [2 Missing :warning: ](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) |
| [src/agents/llama\_guard.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Fllama_guard.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9sbGFtYV9ndWFyZC5weQ==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) |
| [src/client/client.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fclient%2Fclient.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2NsaWVudC9jbGllbnQucHk=) | 66.66% | [2 Missing :warning: ](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) |
| [src/agents/bg\_task\_agent/bg\_task\_agent.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Fbg_task_agent%2Fbg_task_agent.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9iZ190YXNrX2FnZW50L2JnX3Rhc2tfYWdlbnQucHk=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) |
| [src/agents/chatbot.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Fchatbot.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9jaGF0Ym90LnB5) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) |
| [src/agents/interrupt\_agent.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Finterrupt_agent.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9pbnRlcnJ1cHRfYWdlbnQucHk=) | 80.00% | [1 Missing :warning: ](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) |
| [src/core/llm.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fcore%2Fllm.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2NvcmUvbGxtLnB5) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) |
| [src/schema/task\_data.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fschema%2Ftask_data.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL3NjaGVtYS90YXNrX2RhdGEucHk=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) |

| [Files with missing lines](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) | Coverage Δ | |
|---|---|---|
| [src/agents/agents.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Fagents.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9hZ2VudHMucHk=) | `100.00% <100.00%> (ø)` | |
| [src/core/settings.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fcore%2Fsettings.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2NvcmUvc2V0dGluZ3MucHk=) | `75.83% <100.00%> (ø)` | |
| [src/schema/schema.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fschema%2Fschema.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL3NjaGVtYS9zY2hlbWEucHk=) | `85.71% <100.00%> (ø)` | |
| [src/service/service.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fservice%2Fservice.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL3NlcnZpY2Uvc2VydmljZS5weQ==) | `82.82% <100.00%> (ø)` | |
| [src/agents/bg\_task\_agent/bg\_task\_agent.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Fbg_task_agent%2Fbg_task_agent.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9iZ190YXNrX2FnZW50L2JnX3Rhc2tfYWdlbnQucHk=) | `52.63% <0.00%> (ø)` | |
| [src/agents/chatbot.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Fchatbot.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9jaGF0Ym90LnB5) | `53.84% <0.00%> (-3.30%)` | :arrow_down: |
| [src/agents/interrupt\_agent.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Finterrupt_agent.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9pbnRlcnJ1cHRfYWdlbnQucHk=) | `56.66% <80.00%> (+1.66%)` | :arrow_up: |
| [src/core/llm.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fcore%2Fllm.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2NvcmUvbGxtLnB5) | `77.35% <50.00%> (-1.49%)` | :arrow_down: |
| [src/schema/task\_data.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fschema%2Ftask_data.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL3NjaGVtYS90YXNrX2RhdGEucHk=) | `31.70% <0.00%> (ø)` | |
| [src/agents/bg\_task\_agent/task.py](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree&filepath=src%2Fagents%2Fbg_task_agent%2Ftask.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll#diff-c3JjL2FnZW50cy9iZ190YXNrX2FnZW50L3Rhc2sucHk=) | `32.43% <33.33%> (-0.91%)` | :arrow_down: |
| ... and [3 more](https://app.codecov.io/gh/JoshuaC215/agent-service-toolkit/pull/205?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Joshua+Carroll) | |

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: JoshuaC215
created_at: 2025-04-21T02:10:39Z
body: @maver1ck it's working now for me with a rebase. I would suggest to add mypy to the dev dependencies so it's always available instead of just installing in the CI.

See example passing pipelines and a couple additional commits in #210 (I tried to push to your branch but was unable to)

Note: it makes the non-docker CI stages 4-5x slower 😢  I wish mypy wasn't so slow, not sure how to address that. But they are still faster than the docker stage so I guess it's OK. I wonder if it should be after the test step instead of before, since the tests run much faster 🤔 
author_association: OWNER
user: login: maver1ck
created_at: 2025-04-21T15:49:56Z
body: Hi @JoshuaC215 
I rebased this PR, added mypy to dev deps. 

Regarding mypy speed it is possible to use some kind of cache. But I'm not sure how to set this up.
author_association: CONTRIBUTOR
user: login: JoshuaC215
created_at: 2025-04-21T16:55:59Z
body: Ya it seems like the mypy cache or other approaches are a big pain in the neck to set up 🤷

I also briefly tried setting up and running pyright which is supposed to be faster, but it spit out a bunch of new / different errors so I gave up.

Overall this is great!!! Thanks so much for pushing it through!
author_association: OWNER