mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
Add conversations and turns foundation + DDD (#188)
- Store conversations and turns for: - playground chat - api - New DDD code organisation with container dependency injection - sdk update - streaming api support
This commit is contained in:
parent
659b23ae2b
commit
51a33ab2df
39 changed files with 1474 additions and 525 deletions
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "rowboat"
|
||||
version = "4.0.0"
|
||||
version = "5.0.0"
|
||||
authors = [
|
||||
{ name = "Ramnique Singh", email = "ramnique@rowboatlabs.com" },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .client import Client, StatefulChat
|
||||
from .client import Client
|
||||
from .schema import (
|
||||
ApiMessage,
|
||||
UserMessage,
|
||||
|
|
@ -8,21 +8,4 @@ from .schema import (
|
|||
ToolMessage,
|
||||
ApiRequest,
|
||||
ApiResponse
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
"Client",
|
||||
"StatefulChat",
|
||||
# Message types
|
||||
"ApiMessage",
|
||||
"UserMessage",
|
||||
"SystemMessage",
|
||||
"AssistantMessage",
|
||||
"AssistantMessageWithToolCalls",
|
||||
"ToolMessage",
|
||||
# Request/Response types
|
||||
"ApiRequest",
|
||||
"ApiResponse",
|
||||
]
|
||||
)
|
||||
|
|
@ -1,36 +1,30 @@
|
|||
from typing import Dict, List, Optional, Any, Union
|
||||
from typing import Dict, List, Optional
|
||||
import requests
|
||||
from .schema import (
|
||||
ApiRequest,
|
||||
ApiResponse,
|
||||
ApiMessage,
|
||||
UserMessage,
|
||||
AssistantMessage,
|
||||
AssistantMessageWithToolCalls
|
||||
)
|
||||
|
||||
class Client:
|
||||
def __init__(self, host: str, project_id: str, api_key: str) -> None:
|
||||
self.base_url: str = f'{host}/api/v1/{project_id}/chat'
|
||||
def __init__(self, host: str, projectId: str, apiKey: str) -> None:
|
||||
self.base_url: str = f'{host}/api/v1/{projectId}/chat'
|
||||
self.headers: Dict[str, str] = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {api_key}'
|
||||
'Authorization': f'Bearer {apiKey}'
|
||||
}
|
||||
|
||||
def _call_api(
|
||||
self,
|
||||
messages: List[ApiMessage],
|
||||
state: Optional[Dict[str, Any]] = None,
|
||||
workflow_id: Optional[str] = None,
|
||||
test_profile_id: Optional[str] = None,
|
||||
mock_tools: Optional[Dict[str, str]] = None
|
||||
conversationId: Optional[str] = None,
|
||||
mockTools: Optional[Dict[str, str]] = None
|
||||
) -> ApiResponse:
|
||||
request = ApiRequest(
|
||||
messages=messages,
|
||||
state=state,
|
||||
workflowId=workflow_id,
|
||||
testProfileId=test_profile_id,
|
||||
mockTools=mock_tools
|
||||
conversationId=conversationId,
|
||||
mockTools=mockTools
|
||||
)
|
||||
json_data = request.model_dump()
|
||||
response = requests.post(self.base_url, headers=self.headers, json=json_data)
|
||||
|
|
@ -38,86 +32,23 @@ class Client:
|
|||
if not response.status_code == 200:
|
||||
raise ValueError(f"Error: {response.status_code} - {response.text}")
|
||||
|
||||
response_data = ApiResponse.model_validate(response.json())
|
||||
|
||||
if not response_data.messages:
|
||||
raise ValueError("No response")
|
||||
|
||||
last_message = response_data.messages[-1]
|
||||
if not isinstance(last_message, (AssistantMessage, AssistantMessageWithToolCalls)):
|
||||
raise ValueError("Last message was not an assistant message")
|
||||
return ApiResponse.model_validate(response.json())
|
||||
|
||||
return response_data
|
||||
|
||||
def chat(
|
||||
def run_turn(
|
||||
self,
|
||||
messages: List[ApiMessage],
|
||||
state: Optional[Dict[str, Any]] = None,
|
||||
workflow_id: Optional[str] = None,
|
||||
test_profile_id: Optional[str] = None,
|
||||
mock_tools: Optional[Dict[str, str]] = None,
|
||||
conversationId: Optional[str] = None,
|
||||
mockTools: Optional[Dict[str, str]] = None,
|
||||
) -> ApiResponse:
|
||||
"""Stateless chat method that handles a single conversation turn"""
|
||||
|
||||
# call api
|
||||
response_data = self._call_api(
|
||||
return self._call_api(
|
||||
messages=messages,
|
||||
state=state,
|
||||
workflow_id=workflow_id,
|
||||
test_profile_id=test_profile_id,
|
||||
mock_tools=mock_tools,
|
||||
conversationId=conversationId,
|
||||
mockTools=mockTools,
|
||||
)
|
||||
|
||||
if not response_data.messages[-1].responseType == 'external':
|
||||
raise ValueError("Last message was not an external message")
|
||||
|
||||
return response_data
|
||||
|
||||
class StatefulChat:
|
||||
"""Maintains conversation state across multiple turns"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: Client,
|
||||
workflow_id: Optional[str] = None,
|
||||
test_profile_id: Optional[str] = None,
|
||||
mock_tools: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
self.client = client
|
||||
self.messages: List[ApiMessage] = []
|
||||
self.state: Optional[Dict[str, Any]] = None
|
||||
self.workflow_id = workflow_id
|
||||
self.test_profile_id = test_profile_id
|
||||
self.mock_tools = mock_tools
|
||||
|
||||
def run(self, message: Union[str]) -> str:
|
||||
"""Handle a single user turn in the conversation"""
|
||||
|
||||
# Process the message
|
||||
user_msg = UserMessage(role='user', content=message)
|
||||
self.messages.append(user_msg)
|
||||
|
||||
# Get response using the client's chat method
|
||||
response_data = self.client.chat(
|
||||
messages=self.messages,
|
||||
state=self.state,
|
||||
workflow_id=self.workflow_id,
|
||||
test_profile_id=self.test_profile_id,
|
||||
mock_tools=self.mock_tools,
|
||||
)
|
||||
|
||||
# Update internal state
|
||||
self.messages.extend(response_data.messages)
|
||||
self.state = response_data.state
|
||||
|
||||
# Return only the final message content
|
||||
last_message = self.messages[-1]
|
||||
return last_message.content
|
||||
|
||||
|
||||
def weather_lookup_tool(city_name: str) -> str:
|
||||
return f"The weather in {city_name} is 22°C."
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host: str = "<HOST>"
|
||||
|
|
@ -125,13 +56,18 @@ if __name__ == "__main__":
|
|||
api_key: str = "<API_KEY>"
|
||||
client = Client(host, project_id, api_key)
|
||||
|
||||
result = client.chat(
|
||||
result = client.run_turn(
|
||||
messages=[
|
||||
UserMessage(role='user', content="Hello")
|
||||
UserMessage(role='user', content="list my github repos")
|
||||
]
|
||||
)
|
||||
print(result.messages[-1].content)
|
||||
print(result.turn.output[-1].content)
|
||||
print(result.conversationId)
|
||||
|
||||
chat_session = StatefulChat(client)
|
||||
resp = chat_session.run("Hello")
|
||||
print(resp)
|
||||
result = client.run_turn(
|
||||
messages=[
|
||||
UserMessage(role='user', content="how many did you find?")
|
||||
],
|
||||
conversationId=result.conversationId
|
||||
)
|
||||
print(result.turn.output[-1].content)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import List, Optional, Union, Any, Literal, Dict
|
||||
from typing import List, Optional, Union, Literal, Dict
|
||||
from pydantic import BaseModel
|
||||
|
||||
class SystemMessage(BaseModel):
|
||||
|
|
@ -44,13 +44,15 @@ ApiMessage = Union[
|
|||
ToolMessage
|
||||
]
|
||||
|
||||
class Turn(BaseModel):
|
||||
id: str
|
||||
output: List[ApiMessage]
|
||||
|
||||
class ApiRequest(BaseModel):
|
||||
conversationId: Optional[str] = None
|
||||
messages: List[ApiMessage]
|
||||
state: Any
|
||||
workflowId: Optional[str] = None
|
||||
testProfileId: Optional[str] = None
|
||||
mockTools: Optional[Dict[str, str]] = None
|
||||
|
||||
class ApiResponse(BaseModel):
|
||||
messages: List[ApiMessage]
|
||||
state: Optional[Any] = None
|
||||
conversationId: str
|
||||
turn: Turn
|
||||
Loading…
Add table
Add a link
Reference in a new issue