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:
Ramnique Singh 2025-08-05 14:40:48 +05:30 committed by GitHub
parent 659b23ae2b
commit 51a33ab2df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1474 additions and 525 deletions

View file

@ -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" },
]

View file

@ -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",
]
)

View file

@ -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)

View file

@ -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