11from typing import AsyncGenerator , Dict , Protocol , Literal
22from typing import Literal , TypedDict
3- from anthropic import Anthropic
3+ from anthropic import AsyncAnthropic
44from openai import AsyncOpenAI
55from app .core .config import settings
6+ import json
7+ import logging
8+
9+ logger = logging .getLogger (__name__ )
610
711class Message (TypedDict ):
812 role : Literal ["user" , "assistant" , "system" ]
913 content : str
1014
11- class BaseAIClient :
15+ class BaseAIClient ( Protocol ) :
1216 async def chat (self , messages : list [Message ], system : str | None = None ) -> str :
1317 raise NotImplementedError
18+
19+ async def chat_stream (self , messages : list [Message ], system : str | None = None ) -> AsyncGenerator [str , None ]:
20+ raise NotImplementedError
1421
1522class AnthropicClient (BaseAIClient ):
1623 def __init__ (self ):
17- self .client = Anthropic (api_key = settings .ANTHROPIC_API_KEY )
24+ self .client = AsyncAnthropic (api_key = settings .ANTHROPIC_API_KEY )
1825 self .model = settings .ANTHROPIC_MODEL
1926
2027 async def chat (self , messages : list [Message ], system : str | None = None ) -> str :
@@ -27,6 +34,26 @@ async def chat(self, messages: list[Message], system: str | None = None) -> str:
2734 )
2835 return response .content [0 ].text
2936
37+ async def chat_stream (self , messages : list [Message ], system : str | None = None ) -> AsyncGenerator [str , None ]:
38+ """Stream chat responses."""
39+ request_params = {
40+ 'messages' : [{"role" : m ["role" ], "content" : m ["content" ]} for m in messages ],
41+ 'model' : self .model ,
42+ 'max_tokens' : settings .MAX_TOKENS ,
43+ 'temperature' : settings .TEMPERATURE ,
44+ }
45+ if system :
46+ request_params ['system' ] = system
47+
48+ async with self .client .messages .stream (** request_params ) as stream :
49+ async for text in stream .text_stream :
50+ yield f"data: { json .dumps ({'type' : 'content' , 'content' : text })} \n \n "
51+
52+ # Get the final message for history
53+ message = await stream .get_final_message ()
54+ self .add_message ("assistant" , message .content ) # Add to history
55+ yield f"data: { json .dumps ({'type' : 'done' , 'content' : '' })} \n \n "
56+
3057class OpenAIClient (BaseAIClient ):
3158 def __init__ (self ):
3259 self .client = AsyncOpenAI (api_key = settings .OPENAI_API_KEY )
@@ -44,6 +71,22 @@ async def chat(self, messages: list[Message], system: str | None = None) -> str:
4471 )
4572 return response .choices [0 ].message .content
4673
74+ async def chat_stream (self , messages : list [Message ], system : str | None = None ) -> AsyncGenerator [str , None ]:
75+ """Stream chat responses."""
76+ if system :
77+ messages = [{"role" : "system" , "content" : system }, * messages ]
78+
79+ response = await self .client .chat .completions .create (
80+ model = self .model ,
81+ temperature = settings .TEMPERATURE ,
82+ max_tokens = settings .MAX_TOKENS ,
83+ messages = [{"role" : m ["role" ], "content" : m ["content" ]} for m in messages ],
84+ stream = True
85+ )
86+ async for chunk in response :
87+ if chunk .choices :
88+ yield chunk .choices [0 ].delta .content
89+
4790class ChatManager :
4891 def __init__ (self , client : Literal ["anthropic" , "openai" ] = "anthropic" ):
4992 self .history : list [Message ] = []
@@ -57,3 +100,15 @@ async def send_message(self, content: str, system: str | None = None) -> str:
57100 response = await self .client .chat (self .history , system )
58101 self .add_message ("assistant" , response )
59102 return response
103+
104+ async def stream_message (self , content : str , system : str | None = None ) -> AsyncGenerator [str , None ]:
105+ """Stream a message response."""
106+ self .add_message ("user" , content )
107+ async for chunk in self .client .chat_stream (self .history , system ):
108+ yield chunk
109+ # Add the complete message to history after streaming
110+ if self .history [- 1 ]["role" ] == "user" :
111+ last_chunk = None
112+ async for chunk in self .client .chat_stream (self .history , system ):
113+ last_chunk = chunk
114+ self .add_message ("assistant" , last_chunk ) # Add the last chunk as the complete response
0 commit comments