Repository: mito-ds/mito
Pull request #1705: initial commit for anthropic client
Author: login: sid-sagacollab
Created: 2025-05-21T20:46:55Z | Merged: 2025-05-24T05:03:32Z

# Description

This is to enable support for anthropic models/

# Testing

Please provide a list of the ways you can "access" or use the functionality. Please try and be exhaustive here, and make sure that you test everything you list.

- [x] I have tested this on real data that is reasonable and large
- [ ] If I changed the interaction with JupyterLab, I tested that it does not break other programs (like VS Code), and tested that it works "multiple times" in the same notebook.



Diff:
diff --git a/mito-ai/mito_ai/anthropic_client.py b/mito-ai/mito_ai/anthropic_client.py
new file mode 100644
index 0000000000..6baed6a70d
--- /dev/null
+++ b/mito-ai/mito_ai/anthropic_client.py
@@ -0,0 +1,176 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Tuple, Union, Callable, List
+
+from anthropic.types import Message, MessageParam, ToolUnionParam
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem, AgentResponse
+from openai.types.chat import ChatCompletionMessageParam
+
+MAX_TOKENS = 2_000
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _get_system_prompt_and_messages(messages: List[ChatCompletionMessageParam]) -> Tuple[Union[str, anthropic.NotGiven], List[MessageParam]]:
+    """
+    Convert a list of OpenAI messages to a list of Anthropic messages.
+    """
+    anthropic_messages: List[MessageParam] = []
+    system_prompt: Union[str, anthropic.NotGiven] = anthropic.NotGiven()
+    
+    for message in messages:
+        if 'content' not in message:
+            continue
+        
+        # We assume that the converastion only has one system message.
+        # Or if there are multiple, we take the last one.
+        if message['role'] == 'system':
+            system_prompt = str(message['content'])
+
+        # Construct the messages for the user and assistant in Anthropic format.
+        if message['role'] == 'user':
+            anthropic_messages.append(MessageParam(role='user', content=str(message['content'])))
+        elif message['role'] == 'assistant':
+            anthropic_messages.append(MessageParam(role='assistant', content=str(message['content'])))
+
+    return system_prompt, anthropic_messages
+
+
+class AnthropicClient:
+    """
+    A client for interacting with the Anthropic API.
+    """
+
+    def __init__(self, api_key: str, model: str):
+        self.client = anthropic.Anthropic(api_key=api_key)
+        self.model = model
+
+    async def request_completions(self, messages: List[ChatCompletionMessageParam],
+                                  response_format_info: Optional[ResponseFormatInfo] = None) -> Any:
+        """
+        Get a response from Claude that adheres to the AgentResponse format.
+        """
+        try:
+            anthropic_system_prompt, anthropic_messages = _get_system_prompt_and_messages(messages)
+            
+            response: Message
+
+            if response_format_info and response_format_info.name == "agent_response":
+                # Define the tool for structured output
+                tools: List[ToolUnionParam] = [{
+                    "name": "agent_response",
+                    "description": "Output a structured response following the AgentResponse format",
+                    "input_schema": AgentResponse.model_json_schema()
+                }]             
+
+                # Make the API request with tool use
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=MAX_TOKENS,
+                    temperature=0,
+                    system=anthropic_system_prompt,
+                    messages=anthropic_messages,
+                    tools=tools,
+                    tool_choice={"type": "tool", "name": "agent_response"}
+                )
+
+                result = _extract_and_parse_json_response(response)
+                return json.dumps(result) if not isinstance(result, str) else result
+
+            else:
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=MAX_TOKENS,
+                    temperature=0,
+                    system=anthropic_system_prompt,
+                    messages=anthropic_messages,
+                )
+                
+                content = response.content
+                if content[0].type == "text":
+                    return content[0].text
+                else:
+                    return ""
+
+        except anthropic.RateLimitError:
+            raise Exception("Rate limit exceeded. Please try again later or reduce your request frequency.")
+
+        except Exception as e:
+            return f"Error streaming content: {str(e)}"
+
+    async def stream_response(self, messages: List[ChatCompletionMessageParam], message_id: str,
+                              reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None]) -> str:
+        try:
+            anthropic_system_prompt, anthropic_messages = _get_system_prompt_and_messages(messages)
+            accumulated_response = ""
+
+            stream = self.client.messages.create(
+                model=self.model,
+                max_tokens=MAX_TOKENS,
+                temperature=0,
+                system=anthropic_system_prompt,
+                messages=anthropic_messages,
+                stream=True
+            )
+
+            for chunk in stream:
+                if chunk.type == "content_block_delta" and chunk.delta.type == "text_delta":
+                    content = chunk.delta.text 
+                    accumulated_response += content
+
+                    is_finished = chunk.type == "message_stop"
+
+                    reply_fn(CompletionStreamChunk(
+                        parent_id=message_id,
+                        chunk=CompletionItem(
+                            content=content,
+                            isIncomplete=not is_finished,
+                            token=message_id,
+                        ),
+                        done=is_finished,
+                    ))
+
+            return accumulated_response
+
+        except anthropic.RateLimitError:
+            raise Exception("Rate limit exceeded. Please try again later or reduce your request frequency.")
+
+        except Exception as e:
+            return f"Error streaming content: {str(e)}"
+
+
diff --git a/mito-ai/mito_ai/completions/providers.py b/mito-ai/mito_ai/completions/providers.py
index b995e27ab6..e6df55b455 100644
--- a/mito-ai/mito_ai/completions/providers.py
+++ b/mito-ai/mito_ai/completions/providers.py
@@ -13,6 +13,7 @@
 from mito_ai.enterprise.utils import is_azure_openai_configured
 from mito_ai.gemini_client import GeminiClient
 from mito_ai.openai_client import OpenAIClient
+from mito_ai.anthropic_client import AnthropicClient
 from mito_ai.logger import get_logger
 from mito_ai.completions.models import (
     AICapabilities,
@@ -35,6 +36,7 @@
 
 __all__ = ["OpenAIProvider"]
 
+
 class OpenAIProvider(LoggingConfigurable):
     """Provide AI feature through OpenAI services."""
 
@@ -58,15 +60,16 @@ def __init__(self, **kwargs: Dict[str, Any]) -> None:
         if 'api_key' in kwargs:
             config['OpenAIClient'] = {'api_key': kwargs['api_key']}
         kwargs['config'] = config
-        
+
         super().__init__(log=get_logger(), **kwargs)
         self.last_error = None
         self._openai_client: Optional[OpenAIClient] = None
         self._gemini_client: Optional[GeminiClient] = None
-        
+        self._anthropic_client: Optional[AnthropicClient] = None
+
         # Initialize OpenAI client with the configured api_key
         self._openai_client = OpenAIClient(parent=self)
-        
+
         # Initialize Gemini client if configured
         if constants.GEMINI_MODEL and constants.GEMINI_API_KEY:
             self._gemini_client = GeminiClient(
@@ -74,9 +77,24 @@ def __init__(self, **kwargs: Dict[str, Any]) -> None:
                 model=constants.GEMINI_MODEL
             )
 
+        # Initialize Anthropic client if configured
+        if constants.CLAUDE_MODEL and constants.CLAUDE_API_KEY:
+            self._anthropic_client = AnthropicClient(
+                api_key=constants.CLAUDE_API_KEY,
+                model=constants.CLAUDE_MODEL
+            )
+
     @property
     def capabilities(self) -> AICapabilities:
         """Get the provider capabilities."""
+        if constants.CLAUDE_MODEL and constants.CLAUDE_API_KEY and not self.api_key:
+            return AICapabilities(
+                configuration={
+                    "model": constants.CLAUDE_MODEL
+                },
+                provider="Claude",
+            )
+
         if constants.GEMINI_MODEL and constants.GEMINI_API_KEY and not self.api_key:
             return AICapabilities(
                 configuration={
@@ -84,10 +102,10 @@ def capabilities(self) -> AICapabilities:
                 },
                 provider="Gemini",
             )
-            
+
         if self._openai_client:
             return self._openai_client.capabilities
-            
+
         return AICapabilities(
             configuration={
                 "model": "gpt-4.1",
@@ -98,12 +116,15 @@ def capabilities(self) -> AICapabilities:
     @property
     def key_type(self) -> str:
         """Returns the authentication key type being used."""
+        if constants.CLAUDE_MODEL and constants.CLAUDE_API_KEY and not self.api_key:
+            return "claude"
+
         if constants.GEMINI_MODEL and constants.GEMINI_API_KEY and not self.api_key:
             return "gemini"
-            
+
         if self._openai_client:
             return self._openai_client.key_type
-            
+
         return MITO_SERVER_KEY
 
     async def request_completions(
@@ -131,14 +152,24 @@ async def request_completions(
         last_message_content = str(messages[-1].get('content', '')) if messages else ""
 
         try:
+            # Handle Claude API calls
+            if constants.CLAUDE_MODEL and constants.CLAUDE_API_KEY and not self.api_key:
+                if self._anthropic_client is None:
+                    self._anthropic_client = AnthropicClient(
+                        api_key=constants.CLAUDE_API_KEY,
+                        model=model
+                    )
+
+                completion = await self._anthropic_client.request_completions(messages, response_format_info)
+
             # Handle Gemini API calls
-            if constants.GEMINI_MODEL and constants.GEMINI_API_KEY and not self.api_key:
+            elif constants.GEMINI_MODEL and constants.GEMINI_API_KEY and not self.api_key:
                 if self._gemini_client is None:
                     self._gemini_client = GeminiClient(
                         api_key=constants.GEMINI_API_KEY,
                         model=model
                     )
-                
+
                 completion = await self._gemini_client.request_completions(messages, response_format_info)
 
             # Handle OpenAI and other providers
@@ -161,7 +192,7 @@ async def request_completions(
                 user_input=user_input or "",
                 thread_id=thread_id or ""
             )
-            
+
             return completion
 
         except BaseException as e:
@@ -169,17 +200,17 @@ async def request_completions(
             self.last_error = CompletionError.from_exception(e)
             log(MITO_AI_COMPLETION_ERROR, params={KEY_TYPE_PARAM: self.key_type}, error=e)
             raise
-    
+
     async def stream_completions(
-        self,
-        message_type: MessageType,
-        messages: List[ChatCompletionMessageParam],
-        model: str,
-        message_id: str,
-        thread_id: str,
-        reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None],
-        user_input: Optional[str] = None,
-        response_format_info: Optional[ResponseFormatInfo] = None
+            self,
+            message_type: MessageType,
+            messages: List[ChatCompletionMessageParam],
+            model: str,
+            message_id: str,
+            thread_id: str,
+            reply_fn: Callable[[Union[CompletionReply, CompletionStreamChunk]], None],
+            user_input: Optional[str] = None,
+            response_format_info: Optional[ResponseFormatInfo] = None
     ) -> str:
         """
         Stream completions from the AI provider and return the accumulated response.
@@ -189,7 +220,7 @@ async def stream_completions(
         self.last_error = None
         accumulated_response = ""
         last_message_content = str(messages[-1].get('content', '')) if messages else ""
-        
+
         # Send initial acknowledgment
         reply_fn(CompletionReply(
             items=[
@@ -199,14 +230,28 @@ async def stream_completions(
         ))
 
         try:
+            # Handle Claude API calls
+            if constants.CLAUDE_MODEL and constants.CLAUDE_API_KEY and not self.api_key:
+                if self._anthropic_client is None:
+                    self._anthropic_client = AnthropicClient(
+                        api_key=constants.CLAUDE_API_KEY,
+                        model=model
+                    )
+
+                accumulated_response = await self._anthropic_client.stream_response(
+                    messages=messages,
+                    message_id=message_id,
+                    reply_fn=reply_fn
+                )
+
             # Handle Gemini API calls
-            if constants.GEMINI_MODEL and constants.GEMINI_API_KEY and not self.api_key:
+            elif constants.GEMINI_MODEL and constants.GEMINI_API_KEY and not self.api_key:
                 if self._gemini_client is None:
                     self._gemini_client = GeminiClient(
                         api_key=constants.GEMINI_API_KEY,
                         model=model
                     )
-                
+
                 accumulated_response = await self._gemini_client.stream_completions(
                     messages=messages,
                     message_id=message_id,
@@ -237,7 +282,7 @@ async def stream_completions(
                 user_input=user_input or "",
                 thread_id=thread_id
             )
-            
+
             return accumulated_response
 
         except BaseException as e:
@@ -265,3 +310,4 @@ async def stream_completions(
                 error=CompletionError.from_exception(e),
             ))
             raise
+
diff --git a/mito-ai/mito_ai/constants.py b/mito-ai/mito_ai/constants.py
index bcbe0a4f1f..e95fd57cf5 100644
--- a/mito-ai/mito_ai/constants.py
+++ b/mito-ai/mito_ai/constants.py
@@ -5,12 +5,10 @@
 
 # Claude Base URL
 CLAUDE_MODEL = os.environ.get("CLAUDE_MODEL")
-CLAUDE_BASE_URL = "https://api.anthropic.com/v1"
 CLAUDE_API_KEY = os.environ.get("CLAUDE_API_KEY")
 
 # Gemini Base URL
 GEMINI_MODEL = os.environ.get("GEMINI_MODEL")
-GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
 GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
 
 # Ollama Config
diff --git a/mito-ai/mito_ai/tests/providers_test.py b/mito-ai/mito_ai/tests/providers_test.py
index 9a8fb3f564..bfeae0675b 100644
--- a/mito-ai/mito_ai/tests/providers_test.py
+++ b/mito-ai/mito_ai/tests/providers_test.py
@@ -101,6 +101,22 @@ def mock_azure_openai_client() -> Any:
     return patch("mito_ai.completions.providers.OpenAIClient", return_value=mock_client)
 
 
+
+def mock_claude_client() -> Any:
+    """Mock the Claude client capabilities."""
+    mock_client = MagicMock()
+    mock_client.capabilities = AICapabilities(
+        configuration={"model": "claude-3-opus-20240229"},
+        provider="Claude",
+        type="ai_capabilities"
+    )
+    mock_client.key_type = "claude"
+    mock_client.request_completions = AsyncMock(return_value="Test completion")
+    mock_client.stream_completions = AsyncMock(return_value="Test completion")
+    mock_client.stream_response = AsyncMock(return_value="Test completion")
+    return patch("mito_ai.completions.providers.AnthropicClient", return_value=mock_client)
+
+
 def test_os_user_openai_key_set_below_limit(monkeypatch: pytest.MonkeyPatch, provider_config: Config) -> None:
     monkeypatch.setenv("OPENAI_API_KEY", FAKE_API_KEY)
     monkeypatch.setattr("mito_ai.constants.OPENAI_API_KEY", FAKE_API_KEY)
@@ -187,6 +203,20 @@ def test_azure_openai_provider(monkeypatch: pytest.MonkeyPatch, provider_config:
         assert capabilities.configuration["model"] == "gpt-4o"
 
 
+def test_claude_provider(monkeypatch: pytest.MonkeyPatch, provider_config: Config) -> None:
+    monkeypatch.setenv("CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setenv("CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.OPENAI_API_KEY", None)
+
+    with mock_claude_client():
+        llm = OpenAIProvider(config=provider_config)
+        capabilities = llm.capabilities
+        assert capabilities.provider == "Claude"
+        assert capabilities.configuration["model"] == "claude-3-opus-20240229"
+
+
 def test_provider_priority_order(monkeypatch: pytest.MonkeyPatch, provider_config: Config) -> None:
     # Set up all possible providers
     monkeypatch.setattr("mito_ai.enterprise.utils.is_enterprise", lambda: True)
@@ -197,13 +227,22 @@ def test_provider_priority_order(monkeypatch: pytest.MonkeyPatch, provider_confi
     monkeypatch.setattr("mito_ai.enterprise.utils.AZURE_OPENAI_API_VERSION", "2024-12-01-preview")
     monkeypatch.setenv("OPENAI_API_KEY", FAKE_API_KEY)
     monkeypatch.setattr("mito_ai.constants.OPENAI_API_KEY", FAKE_API_KEY)
+    monkeypatch.setenv("CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setenv("CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_MODEL", "claude-3-opus-20240229")
 
     # Azure OpenAI should have highest priority when enterprise is enabled
-    # Clear Gemini settings to ensure Azure OpenAI is selected
+    # Clear other provider settings to ensure Azure OpenAI is selected
     monkeypatch.delenv("GEMINI_API_KEY", raising=False)
     monkeypatch.delenv("GEMINI_MODEL", raising=False)
     monkeypatch.setattr("mito_ai.constants.GEMINI_API_KEY", None)
     monkeypatch.setattr("mito_ai.constants.GEMINI_MODEL", None)
+    # Clear Claude settings to ensure Azure OpenAI is selected
+    monkeypatch.delenv("CLAUDE_API_KEY", raising=False)
+    monkeypatch.delenv("CLAUDE_MODEL", raising=False)
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_API_KEY", None)
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_MODEL", None)
     with mock_azure_openai_client():
         llm = OpenAIProvider(config=provider_config)
         capabilities = llm.capabilities
@@ -217,20 +256,20 @@ def test_provider_priority_order(monkeypatch: pytest.MonkeyPatch, provider_confi
         capabilities = llm.capabilities
         assert capabilities.provider == "OpenAI with user key"
 
-    # Without OpenAI key, Gemini should be used
+    # Without OpenAI key, Claude should be used (higher priority than Gemini)
     monkeypatch.delenv("OPENAI_API_KEY")
     monkeypatch.setattr("mito_ai.constants.OPENAI_API_KEY", None)
-    # Set up Gemini settings
-    monkeypatch.setenv("GEMINI_API_KEY", "gemini-key")
-    monkeypatch.setenv("GEMINI_MODEL", "gemini-2-pro")
-    monkeypatch.setattr("mito_ai.constants.GEMINI_API_KEY", "gemini-key")
-    monkeypatch.setattr("mito_ai.constants.GEMINI_MODEL", "gemini-2-pro")
     # Ensure provider_config doesn't have an api_key set
     provider_config.OpenAIProvider.api_key = None
-    with mock_gemini_client():
+    # Re-enable Claude settings
+    monkeypatch.setenv("CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setenv("CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_MODEL", "claude-3-opus-20240229")
+    with mock_claude_client():
         llm = OpenAIProvider(config=provider_config)
         capabilities = llm.capabilities
-        assert capabilities.provider == "Gemini"
+        assert capabilities.provider == "Claude"
 
 
 @pytest.mark.asyncio
@@ -318,4 +357,103 @@ def test_error_handling(monkeypatch: pytest.MonkeyPatch, provider_config: Config
 
     with patch("mito_ai.completions.providers.OpenAIClient", return_value=mock_client):
         llm = OpenAIProvider(config=provider_config)
-        assert llm.last_error is None  # Error should be None until a request is made
\ No newline at end of file
+        assert llm.last_error is None  # Error should be None until a request is made
+
+
+@pytest.mark.asyncio
+async def test_claude_completion_request(monkeypatch: pytest.MonkeyPatch, provider_config: Config) -> None:
+    monkeypatch.setenv("CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setenv("CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.OPENAI_API_KEY", None)
+
+    mock_client = MagicMock()
+    mock_client.capabilities = AICapabilities(
+        configuration={"model": "claude-3-opus-20240229"},
+        provider="Claude",
+        type="ai_capabilities"
+    )
+    mock_client.key_type = "claude"
+    mock_client.request_completions = AsyncMock(return_value="Test completion")
+    mock_client.stream_completions = AsyncMock(return_value="Test completion")
+
+    with patch("mito_ai.completions.providers.AnthropicClient", return_value=mock_client):
+        llm = OpenAIProvider(config=provider_config)
+        messages: List[ChatCompletionMessageParam] = [
+            {"role": "user", "content": "Test message"}
+        ]
+
+        completion = await llm.request_completions(
+            message_type=MessageType.CHAT,
+            messages=messages,
+            model="claude-3-opus-20240229"
+        )
+
+        assert completion == "Test completion"
+        mock_client.request_completions.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_claude_stream_completion(monkeypatch: pytest.MonkeyPatch, provider_config: Config) -> None:
+    monkeypatch.setenv("CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setenv("CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_API_KEY", "claude-key")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.OPENAI_API_KEY", None)
+
+    mock_client = MagicMock()
+    mock_client.capabilities = AICapabilities(
+        configuration={"model": "claude-3-opus-20240229"},
+        provider="Claude",
+        type="ai_capabilities"
+    )
+    mock_client.key_type = "claude"
+    mock_client.request_completions = AsyncMock(return_value="Test completion")
+    mock_client.stream_completions = AsyncMock(return_value="Test completion")
+    mock_client.stream_response = AsyncMock(return_value="Test completion")
+
+    with patch("mito_ai.completions.providers.AnthropicClient", return_value=mock_client):
+        llm = OpenAIProvider(config=provider_config)
+        messages: List[ChatCompletionMessageParam] = [
+            {"role": "user", "content": "Test message"}
+        ]
+
+        reply_chunks = []
+        def mock_reply(chunk):
+            reply_chunks.append(chunk)
+
+        completion = await llm.stream_completions(
+            message_type=MessageType.CHAT,
+            messages=messages,
+            model="claude-3-opus-20240229",
+            message_id="test-id",
+            thread_id="test-thread",
+            reply_fn=mock_reply
+        )
+
+        assert completion == "Test completion"
+        mock_client.stream_response.assert_called_once()
+        assert len(reply_chunks) > 0
+        assert isinstance(reply_chunks[0], CompletionReply)
+
+
+def test_claude_error_handling(monkeypatch: pytest.MonkeyPatch, provider_config: Config) -> None:
+    monkeypatch.setenv("CLAUDE_API_KEY", "invalid-key")
+    monkeypatch.setenv("CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_API_KEY", "invalid-key")
+    monkeypatch.setattr("mito_ai.constants.CLAUDE_MODEL", "claude-3-opus-20240229")
+    monkeypatch.setattr("mito_ai.constants.OPENAI_API_KEY", None)
+
+    mock_client = MagicMock()
+    mock_client.capabilities = AICapabilities(
+        configuration={"model": "claude-3-opus-20240229"},
+        provider="Claude",
+        type="ai_capabilities"
+    )
+    mock_client.key_type = "claude"
+    mock_client.request_completions.side_effect = Exception("API error")
+
+    with patch("mito_ai.completions.providers.AnthropicClient", return_value=mock_client):
+      llm = OpenAIProvider(config=provider_config)
+      assert llm.last_error is None  # Error should be None until a request is made
\ No newline at end of file
diff --git a/mito-ai/pyproject.toml b/mito-ai/pyproject.toml
index 06e26745e8..b4d255af17 100644
--- a/mito-ai/pyproject.toml
+++ b/mito-ai/pyproject.toml
@@ -41,7 +41,8 @@ dependencies = [
     "SQLAlchemy",
     "snowflake-sqlalchemy",
     "pipreqs",
-    "google-genai"
+    "google-genai",
+    "anthropic"
 ]
 dynamic = ["version", "description", "authors", "urls", "keywords"]
 


Reviews:
user: login: aarondr77
body: Nice work. This is working pretty well (except I keep getting rate limited). I think we need to use our Claude keys more so they bump our Anthropic tier! I have a few questions here about the anthropic speficic stuff.

Let me know what you think. 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T15:59:18Z
user: login: sid-sagacollab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:40:40Z
user: login: sid-sagacollab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:40:43Z
user: login: sid-sagacollab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:41:11Z
user: login: sid-sagacollab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:41:15Z
user: login: sid-sagacollab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:41:28Z
user: login: sid-sagacollab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:41:34Z
user: login: sid-sagacollab
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T19:41:36Z

Review comments:
diff_hunk: @@ -199,14 +230,28 @@ async def stream_completions(
         ))
 
         try:
+            # Handle Claude API calls
+            if constants.CLAUDE_MODEL and constants.CLAUDE_API_KEY and not self.api_key:
+                if self._anthropic_client is None:
+                    self._anthropic_client = AnthropicClient(
+                        api_key=constants.CLAUDE_API_KEY,
+                        model=model
+                    )
+
+                accumulated_response = await self._anthropic_client.stream_response(
+                    messages=messages,
+                    message_id=message_id,
+                    reply_fn=reply_fn
+                )
path: mito-ai/mito_ai/completions/providers.py
user: login: aarondr77
body: Sweet! It's awesome how this code is so simple for new clients. Nice!
created_at: 2025-05-22T15:23:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
path: mito-ai/mito_ai/anthropic_client.py
user: login: aarondr77
body: Is it possible to auto generate this from the Pydantic base class? That way we don't need to make sure we're keeping both in sync?
created_at: 2025-05-22T15:25:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
+    """
+    Convert a list of chat messages to a single prompt string for Claude.
+
+    Args:
+        messages: List of chat messages in OpenAI format
+
+    Returns:
+        A single string prompt formatted for Claude
+    """
+    # Filter out messages without content and convert to string format
+    formatted_messages = []
+    for msg in messages:
+        if isinstance(msg, dict) and 'content' in msg and msg['content']:
+            role = msg.get('role', 'user')
path: mito-ai/mito_ai/anthropic_client.py
user: login: aarondr77
body: In what cases would the role not be set? 
created_at: 2025-05-22T15:27:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
+    """
+    Convert a list of chat messages to a single prompt string for Claude.
+
+    Args:
+        messages: List of chat messages in OpenAI format
+
+    Returns:
+        A single string prompt formatted for Claude
+    """
+    # Filter out messages without content and convert to string format
+    formatted_messages = []
+    for msg in messages:
+        if isinstance(msg, dict) and 'content' in msg and msg['content']:
+            role = msg.get('role', 'user')
+            content = msg['content']
+            if isinstance(content, str):
+                formatted_messages.append(f"{role}: {content}")
+
+    return "\n".join(formatted_messages)
+
+
+class AnthropicClient:
+    """
+    A client for interacting with the Anthropic API.
+    """
+
+    def __init__(self, api_key: str, model: str):
+        self.client = anthropic.Anthropic(api_key=api_key)
+        self.model = model
+
+    async def request_completions(self, messages: List[ChatCompletionMessageParam],
+                                  response_format_info: Optional[ResponseFormatInfo] = None) -> Any:
+        """
+        Get a response from Claude that adheres to the AgentResponse format.
+        """
+        try:
+            prompt = _convert_messages_to_prompt(messages)
+
+            if response_format_info:
+                # Define the tool for structured output
+                tools = [{
+                    "name": "agent_response",
+                    "description": "Output a structured response following the AgentResponse format",
+                    "input_schema": AGENT_RESPONSE_SCHEMA
+                }]
+
+                # Make the API request with tool use
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
+                    temperature=0,
+                    messages=[
+                        {"role": "user", "content": _formatted_prompt(prompt)}
+                    ],
+                    tools=tools,
+                    tool_choice={"type": "tool", "name": "agent_response"}
path: mito-ai/mito_ai/anthropic_client.py
user: login: aarondr77
body: My understanding is that if we set the `tool_choice` field, we are forcing the Claude to always respond with this tool. If that is the case, do we need all of the `_formatted_prompt` stuff? It would obviously be ideal if we didn't need to maintain this additional set of prompt instructions, but if we need it, then we need it. 
created_at: 2025-05-22T15:32:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
+    """
+    Convert a list of chat messages to a single prompt string for Claude.
+
+    Args:
+        messages: List of chat messages in OpenAI format
+
+    Returns:
+        A single string prompt formatted for Claude
+    """
+    # Filter out messages without content and convert to string format
+    formatted_messages = []
+    for msg in messages:
+        if isinstance(msg, dict) and 'content' in msg and msg['content']:
+            role = msg.get('role', 'user')
+            content = msg['content']
+            if isinstance(content, str):
+                formatted_messages.append(f"{role}: {content}")
+
+    return "\n".join(formatted_messages)
+
+
+class AnthropicClient:
+    """
+    A client for interacting with the Anthropic API.
+    """
+
+    def __init__(self, api_key: str, model: str):
+        self.client = anthropic.Anthropic(api_key=api_key)
+        self.model = model
+
+    async def request_completions(self, messages: List[ChatCompletionMessageParam],
+                                  response_format_info: Optional[ResponseFormatInfo] = None) -> Any:
+        """
+        Get a response from Claude that adheres to the AgentResponse format.
+        """
+        try:
+            prompt = _convert_messages_to_prompt(messages)
+
+            if response_format_info:
+                # Define the tool for structured output
+                tools = [{
+                    "name": "agent_response",
+                    "description": "Output a structured response following the AgentResponse format",
+                    "input_schema": AGENT_RESPONSE_SCHEMA
+                }]
+
+                # Make the API request with tool use
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
+                    temperature=0,
+                    messages=[
+                        {"role": "user", "content": _formatted_prompt(prompt)}
+                    ],
+                    tools=tools,
+                    tool_choice={"type": "tool", "name": "agent_response"}
+                )
+
+                result = _extract_and_parse_json_response(response)
+                return json.dumps(result) if not isinstance(result, str) else result
+
+            else:
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
path: mito-ai/mito_ai/anthropic_client.py
user: login: aarondr77
body: Can we create some variables for max_tokens and temperature at the top of this file so we can easily keep them consistent. 

I would also be down to remove max_tokens, I don' think we're setting that for other providers, right?
created_at: 2025-05-22T15:33:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
+    """
+    Convert a list of chat messages to a single prompt string for Claude.
+
+    Args:
+        messages: List of chat messages in OpenAI format
+
+    Returns:
+        A single string prompt formatted for Claude
+    """
+    # Filter out messages without content and convert to string format
+    formatted_messages = []
+    for msg in messages:
+        if isinstance(msg, dict) and 'content' in msg and msg['content']:
+            role = msg.get('role', 'user')
+            content = msg['content']
+            if isinstance(content, str):
+                formatted_messages.append(f"{role}: {content}")
+
+    return "\n".join(formatted_messages)
+
+
+class AnthropicClient:
+    """
+    A client for interacting with the Anthropic API.
+    """
+
+    def __init__(self, api_key: str, model: str):
+        self.client = anthropic.Anthropic(api_key=api_key)
+        self.model = model
+
+    async def request_completions(self, messages: List[ChatCompletionMessageParam],
+                                  response_format_info: Optional[ResponseFormatInfo] = None) -> Any:
+        """
+        Get a response from Claude that adheres to the AgentResponse format.
+        """
+        try:
+            prompt = _convert_messages_to_prompt(messages)
+
+            if response_format_info:
+                # Define the tool for structured output
+                tools = [{
+                    "name": "agent_response",
+                    "description": "Output a structured response following the AgentResponse format",
+                    "input_schema": AGENT_RESPONSE_SCHEMA
+                }]
+
+                # Make the API request with tool use
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
+                    temperature=0,
+                    messages=[
+                        {"role": "user", "content": _formatted_prompt(prompt)}
+                    ],
+                    tools=tools,
+                    tool_choice={"type": "tool", "name": "agent_response"}
+                )
+
+                result = _extract_and_parse_json_response(response)
+                return json.dumps(result) if not isinstance(result, str) else result
+
+            else:
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
+                    temperature=0,
+                    messages=[
+                        {"role": "user", "content": prompt}
path: mito-ai/mito_ai/anthropic_client.py
user: login: aarondr77
body: Is this right? Why are we collapsing the entire conversation into one user prompt?. I think this is supposed to be a list of messages from the conversation. And it looks like the system prompt has its own field

From the docs here: https://docs.anthropic.com/en/docs/get-started#call-the-api

<img width="582" alt="Screenshot 2025-05-22 at 11 38 23 AM" src="https://github.com/user-attachments/assets/b3bb8dd1-9217-479b-946e-ce76fc0c04f3" />

created_at: 2025-05-22T15:39:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
path: mito-ai/mito_ai/anthropic_client.py
user: login: aarondr77
body: I think this function is not properly handling the get_cell_output tool. Even with `claude-3-7-sonnet-20250219`, I'm getting stuck in loops of using the `get_cell_output` tool. 

I think it might be the case that we're not actually sending the cell output to the AI! When we send the cell output, the message is constructed in `create_ai_optimized_message`. Notice that it has `image_url` part to the message as well. 

This might also be the case for Gemini Client! Can we add pytest tests for this `_convert_messages_to_prompt` function and also the gemini one?


<img width="1683" alt="Screenshot 2025-05-22 at 11 47 22 AM" src="https://github.com/user-attachments/assets/99f5baa0-2303-41c8-a037-9c19e4015f1d" />

created_at: 2025-05-22T15:53:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
+    """
+    Convert a list of chat messages to a single prompt string for Claude.
+
+    Args:
+        messages: List of chat messages in OpenAI format
+
+    Returns:
+        A single string prompt formatted for Claude
+    """
+    # Filter out messages without content and convert to string format
+    formatted_messages = []
+    for msg in messages:
+        if isinstance(msg, dict) and 'content' in msg and msg['content']:
+            role = msg.get('role', 'user')
+            content = msg['content']
+            if isinstance(content, str):
+                formatted_messages.append(f"{role}: {content}")
+
+    return "\n".join(formatted_messages)
+
+
+class AnthropicClient:
+    """
+    A client for interacting with the Anthropic API.
+    """
+
+    def __init__(self, api_key: str, model: str):
+        self.client = anthropic.Anthropic(api_key=api_key)
+        self.model = model
+
+    async def request_completions(self, messages: List[ChatCompletionMessageParam],
+                                  response_format_info: Optional[ResponseFormatInfo] = None) -> Any:
+        """
+        Get a response from Claude that adheres to the AgentResponse format.
+        """
+        try:
+            prompt = _convert_messages_to_prompt(messages)
+
+            if response_format_info:
+                # Define the tool for structured output
+                tools = [{
+                    "name": "agent_response",
+                    "description": "Output a structured response following the AgentResponse format",
+                    "input_schema": AGENT_RESPONSE_SCHEMA
+                }]
+
+                # Make the API request with tool use
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
+                    temperature=0,
+                    messages=[
+                        {"role": "user", "content": _formatted_prompt(prompt)}
+                    ],
+                    tools=tools,
+                    tool_choice={"type": "tool", "name": "agent_response"}
path: mito-ai/mito_ai/anthropic_client.py
user: login: aarondr77
body: I tested this a little bit with the `_formatted_prompt` removed and it is adhering to the json output. But maybe you've tested it more than me!
created_at: 2025-05-22T15:58:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
+    """
+    Convert a list of chat messages to a single prompt string for Claude.
+
+    Args:
+        messages: List of chat messages in OpenAI format
+
+    Returns:
+        A single string prompt formatted for Claude
+    """
+    # Filter out messages without content and convert to string format
+    formatted_messages = []
+    for msg in messages:
+        if isinstance(msg, dict) and 'content' in msg and msg['content']:
+            role = msg.get('role', 'user')
+            content = msg['content']
+            if isinstance(content, str):
+                formatted_messages.append(f"{role}: {content}")
+
+    return "\n".join(formatted_messages)
+
+
+class AnthropicClient:
+    """
+    A client for interacting with the Anthropic API.
+    """
+
+    def __init__(self, api_key: str, model: str):
+        self.client = anthropic.Anthropic(api_key=api_key)
+        self.model = model
+
+    async def request_completions(self, messages: List[ChatCompletionMessageParam],
+                                  response_format_info: Optional[ResponseFormatInfo] = None) -> Any:
+        """
+        Get a response from Claude that adheres to the AgentResponse format.
+        """
+        try:
+            prompt = _convert_messages_to_prompt(messages)
+
+            if response_format_info:
+                # Define the tool for structured output
+                tools = [{
+                    "name": "agent_response",
+                    "description": "Output a structured response following the AgentResponse format",
+                    "input_schema": AGENT_RESPONSE_SCHEMA
+                }]
+
+                # Make the API request with tool use
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
+                    temperature=0,
+                    messages=[
+                        {"role": "user", "content": _formatted_prompt(prompt)}
+                    ],
+                    tools=tools,
+                    tool_choice={"type": "tool", "name": "agent_response"}
+                )
+
+                result = _extract_and_parse_json_response(response)
+                return json.dumps(result) if not isinstance(result, str) else result
+
+            else:
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
+                    temperature=0,
+                    messages=[
+                        {"role": "user", "content": prompt}
path: mito-ai/mito_ai/anthropic_client.py
user: login: sid-sagacollab
body: This piece of code gets triggered in two scenarios,

1. For inline completions

2. For setting the title of the chat.

Both of these are concerned with the code in the current cell and the first response from the chat/agent respectively and not the whole conversation history afaik. So, I think this can be as is. Lmk if there is a hole in my understanding and if this needs additional work.

created_at: 2025-05-22T19:40:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
path: mito-ai/mito_ai/anthropic_client.py
user: login: sid-sagacollab
body: will be dealt with here: https://github.com/mito-ds/mito/issues/1707
created_at: 2025-05-22T19:40:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
+    """
+    Convert a list of chat messages to a single prompt string for Claude.
+
+    Args:
+        messages: List of chat messages in OpenAI format
+
+    Returns:
+        A single string prompt formatted for Claude
+    """
+    # Filter out messages without content and convert to string format
+    formatted_messages = []
+    for msg in messages:
+        if isinstance(msg, dict) and 'content' in msg and msg['content']:
+            role = msg.get('role', 'user')
+            content = msg['content']
+            if isinstance(content, str):
+                formatted_messages.append(f"{role}: {content}")
+
+    return "\n".join(formatted_messages)
+
+
+class AnthropicClient:
+    """
+    A client for interacting with the Anthropic API.
+    """
+
+    def __init__(self, api_key: str, model: str):
+        self.client = anthropic.Anthropic(api_key=api_key)
+        self.model = model
+
+    async def request_completions(self, messages: List[ChatCompletionMessageParam],
+                                  response_format_info: Optional[ResponseFormatInfo] = None) -> Any:
+        """
+        Get a response from Claude that adheres to the AgentResponse format.
+        """
+        try:
+            prompt = _convert_messages_to_prompt(messages)
+
+            if response_format_info:
+                # Define the tool for structured output
+                tools = [{
+                    "name": "agent_response",
+                    "description": "Output a structured response following the AgentResponse format",
+                    "input_schema": AGENT_RESPONSE_SCHEMA
+                }]
+
+                # Make the API request with tool use
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
+                    temperature=0,
+                    messages=[
+                        {"role": "user", "content": _formatted_prompt(prompt)}
+                    ],
+                    tools=tools,
+                    tool_choice={"type": "tool", "name": "agent_response"}
+                )
+
+                result = _extract_and_parse_json_response(response)
+                return json.dumps(result) if not isinstance(result, str) else result
+
+            else:
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
path: mito-ai/mito_ai/anthropic_client.py
user: login: sid-sagacollab
body: The `max_tokens` is a mandatory field. I have moved it to the top of the file in my subsequent commit.
created_at: 2025-05-22T19:41:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
+    """
+    Convert a list of chat messages to a single prompt string for Claude.
+
+    Args:
+        messages: List of chat messages in OpenAI format
+
+    Returns:
+        A single string prompt formatted for Claude
+    """
+    # Filter out messages without content and convert to string format
+    formatted_messages = []
+    for msg in messages:
+        if isinstance(msg, dict) and 'content' in msg and msg['content']:
+            role = msg.get('role', 'user')
+            content = msg['content']
+            if isinstance(content, str):
+                formatted_messages.append(f"{role}: {content}")
+
+    return "\n".join(formatted_messages)
+
+
+class AnthropicClient:
+    """
+    A client for interacting with the Anthropic API.
+    """
+
+    def __init__(self, api_key: str, model: str):
+        self.client = anthropic.Anthropic(api_key=api_key)
+        self.model = model
+
+    async def request_completions(self, messages: List[ChatCompletionMessageParam],
+                                  response_format_info: Optional[ResponseFormatInfo] = None) -> Any:
+        """
+        Get a response from Claude that adheres to the AgentResponse format.
+        """
+        try:
+            prompt = _convert_messages_to_prompt(messages)
+
+            if response_format_info:
+                # Define the tool for structured output
+                tools = [{
+                    "name": "agent_response",
+                    "description": "Output a structured response following the AgentResponse format",
+                    "input_schema": AGENT_RESPONSE_SCHEMA
+                }]
+
+                # Make the API request with tool use
+                response = self.client.messages.create(
+                    model=self.model,
+                    max_tokens=2000,
+                    temperature=0,
+                    messages=[
+                        {"role": "user", "content": _formatted_prompt(prompt)}
+                    ],
+                    tools=tools,
+                    tool_choice={"type": "tool", "name": "agent_response"}
path: mito-ai/mito_ai/anthropic_client.py
user: login: sid-sagacollab
body: This additional layer has been added based on this article from anthropic: [https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/increase-consistency#example-standardizing-customer-feedback](https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/increase-consistency#example-standardizing-customer-feedback)
created_at: 2025-05-22T19:41:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "type": {
+            "type": "string",
+            "enum": ["cell_update", "get_cell_output", "finished_task"]
+        },
+        "message": {
+            "type": "string"
+        },
+        "cell_update": {
+            "type": "object",
+            "properties": {
+                "type": {
+                    "type": "string",
+                    "enum": ["new", "modification"]
+                },
+                "index": {
+                    "type": ["integer", "null"]
+                },
+                "id": {
+                    "type": ["string", "null"]
+                },
+                "code": {
+                    "type": "string"
+                },
+                "cell_type": {
+                    "type": ["string", "null"],
+                    "enum": ["code", "markdown", "null"]
+                }
+            },
+            "required": ["type", "code"]
+        },
+        "get_cell_output_cell_id": {
+            "type": ["string", "null"]
+        }
+    },
+    "required": ["type", "message"]
+}
+
+AGENT_RESPONSE_EXAMPLES = {
+    "cell_update_example": {
+        "type": "cell_update",
+        "message": "I've created a new cell with a function to calculate fibonacci numbers.",
+        "cell_update": {
+            "type": "new",
+            "index": 2,
+            "id": None,
+            "code": "def fibonacci(n):\n    if n <= 1:\n        return n\n    return fibonacci(n-1) + fibonacci(n-2)",
+            "cell_type": "code"
+        },
+        "get_cell_output_cell_id": None
+    },
+    "get_output_example": {
+        "type": "get_cell_output",
+        "message": "Let me check the output of the previous cell.",
+        "cell_update": None,
+        "get_cell_output_cell_id": "cell-123"
+    },
+    "finished_task_example": {
+        "type": "finished_task",
+        "message": "I've completed the requested analysis.",
+        "cell_update": None,
+        "get_cell_output_cell_id": None
+    }
+}
+
+
+def _extract_and_parse_json_response(response: Message) -> Union[object, Any]:
+    """
+    Extracts and parses the JSON response from the Claude API.
+    """
+    try:
+        # Check for tool use in the response
+        for content_block in response.content:
+            if content_block.type == "tool_use" and content_block.name == "agent_response":
+                result = content_block.input
+                return result
+
+        # If no tool use was found, try to parse the text response
+        text_response = None
+        for content_block in response.content:
+            if content_block.type == "text":
+                text_response = content_block.text
+                break
+
+        if text_response:
+            # Try to extract JSON from the text response
+            import re
+            json_pattern = r'(\{.*\})'
+            match = re.search(json_pattern, text_response, re.DOTALL)
+            if match:
+                try:
+                    json_response = json.loads(match.group(0))
+                    return json_response
+                except json.JSONDecodeError:
+                    pass
+
+        raise Exception("No valid AgentResponse format found in the response")
+    except Exception as e:
+        raise Exception(f"Failed to parse response: {e}")
+
+
+def _formatted_prompt(prompt: str) -> str:
+    return f"""
+                {prompt}
+
+                I need your response to follow the AgentResponse format exactly. DO NOT add a preamble or any other 
+                text surrounding the JSON response. Stick to the JSON format strictly.
+
+                Here are examples of valid responses:
+
+                Cell Update Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["cell_update_example"], indent=2)}
+
+                Get Cell Output Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["get_output_example"], indent=2)}
+
+                Finished Task Example:
+                {json.dumps(AGENT_RESPONSE_EXAMPLES["finished_task_example"], indent=2)}
+
+                Important requirements:
+                - If type is "cell_update", you MUST include a valid cell_update object with "type" and "code" fields.
+                - If type is "get_cell_output", you MUST include a get_cell_output_cell_id.
+                - For "finished_task", no additional fields are required.
+                - The message field is always required.
+                """
+
+
+def _convert_messages_to_prompt(messages: List[ChatCompletionMessageParam]) -> str:
+    """
+    Convert a list of chat messages to a single prompt string for Claude.
+
+    Args:
+        messages: List of chat messages in OpenAI format
+
+    Returns:
+        A single string prompt formatted for Claude
+    """
+    # Filter out messages without content and convert to string format
+    formatted_messages = []
+    for msg in messages:
+        if isinstance(msg, dict) and 'content' in msg and msg['content']:
+            role = msg.get('role', 'user')
path: mito-ai/mito_ai/anthropic_client.py
user: login: sid-sagacollab
body: This is just a guard rail to prevent the code from breaking when the role is set to `system`.
created_at: 2025-05-22T19:41:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,261 @@
+# Copyright (c) Saga Inc.
+# Distributed under the terms of the GNU Affero General Public License v3.0 License.
+
+import json
+import anthropic
+from typing import Dict, Any, Optional, Union, Callable, List
+
+from anthropic.types import Message
+from mito_ai.completions.models import ResponseFormatInfo, CompletionReply, CompletionStreamChunk, CompletionItem
+from openai.types.chat import ChatCompletionMessageParam
+
+AGENT_RESPONSE_SCHEMA = {
path: mito-ai/mito_ai/anthropic_client.py
user: login: sid-sagacollab
body: Done
created_at: 2025-05-22T19:41:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -199,14 +230,28 @@ async def stream_completions(
         ))
 
         try:
+            # Handle Claude API calls
+            if constants.CLAUDE_MODEL and constants.CLAUDE_API_KEY and not self.api_key:
+                if self._anthropic_client is None:
+                    self._anthropic_client = AnthropicClient(
+                        api_key=constants.CLAUDE_API_KEY,
+                        model=model
+                    )
+
+                accumulated_response = await self._anthropic_client.stream_response(
+                    messages=messages,
+                    message_id=message_id,
+                    reply_fn=reply_fn
+                )
path: mito-ai/mito_ai/completions/providers.py
user: login: sid-sagacollab
body: :)
created_at: 2025-05-22T19:41:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-21T20:47:00Z
body: [vc]: #WKG3kHjT9H3DkcXhWPa9vhPvX62/4J2lAiZFSfgXtSw=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJtb25vcmVwbyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJtb25vcmVwby1naXQtY2xhdWRlLXN1cHBvcnQtdHJ5bWl0by52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS90cnltaXRvL21vbm9yZXBvLzVVenBrRWhvRzZzN3NxTHpXRXJaYkJLWHZmZEsiLCJwcmV2aWV3VXJsIjoibW9ub3JlcG8tZ2l0LWNsYXVkZS1zdXBwb3J0LXRyeW1pdG8udmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOiJ0cnltaXRvLmlvIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **monorepo** | ✅ Ready ([Inspect](https://vercel.com/trymito/monorepo/5UzpkEhoG6s7sqLzWErZbBKXvfdK)) | [Visit Preview](https://monorepo-git-claude-support-trymito.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/monorepo-git-claude-support-trymito.vercel.app?via=pr-comment-feedback-link) | May 24, 2025 4:51am |


author_association: NONE
user: login: aarondr77
created_at: 2025-05-23T14:53:54Z
body: Can you make sure that the `mito-ai src` and `mito-ai mypy` tests are passing before merging. I'll take care of the otheres
author_association: MEMBER