Update docs and simplify sdk

This commit is contained in:
Ramnique Singh 2025-04-10 00:24:47 +05:30
parent c338ec4dec
commit 97feb71869
10 changed files with 265 additions and 446 deletions

View file

@ -12,13 +12,68 @@ pip install rowboat
## Usage
### Basic Usage
### Basic Usage with StatefulChat
Initialize a client and use the chat method directly:
The easiest way to interact with Rowboat is using the `StatefulChat` class, which maintains conversation state automatically:
```python
from rowboat import Client
from rowboat.schema import UserMessage, SystemMessage
from rowboat import Client, StatefulChat
# Initialize the client
client = Client(
host="<HOST>",
project_id="<PROJECT_ID>",
api_key="<API_KEY>"
)
# Create a stateful chat session
chat = StatefulChat(client)
# Have a conversation
response = chat.run("What is the capital of France?")
print(response)
# The capital of France is Paris.
# Continue the conversation - the context is maintained automatically
response = chat.run("What other major cities are in that country?")
print(response)
# Other major cities in France include Lyon, Marseille, Toulouse, and Nice.
response = chat.run("What's the population of the first city you mentioned?")
print(response)
# Lyon has a population of approximately 513,000 in the city proper.
```
### Advanced Usage
#### Using a specific workflow
You can specify a workflow ID to use a particular conversation configuration:
```python
chat = StatefulChat(
client,
workflow_id="<WORKFLOW_ID>"
)
```
#### Using a test profile
You can specify a test profile ID to use a specific test configuration:
```python
chat = StatefulChat(
client,
test_profile_id="<TEST_PROFILE_ID>"
)
```
### Low-Level Usage
For more control over the conversation, you can use the `Client` class directly:
```python
from rowboat.schema import UserMessage
# Initialize the client
client = Client(
@ -29,108 +84,15 @@ client = Client(
# Create messages
messages = [
SystemMessage(role='system', content="You are a helpful assistant"),
UserMessage(role='user', content="Hello, how are you?")
]
# Get response
response_messages, state = client.chat(messages=messages)
print(response_messages[-1].content)
response = client.chat(messages=messages)
print(response.messages[-1].content)
# For subsequent messages, include previous messages and state
messages.extend(response_messages)
# For subsequent messages, you need to manage the message history and state manually
messages.extend(response.messages)
messages.append(UserMessage(role='user', content="What's your name?"))
response_messages, state = client.chat(messages=messages, state=state)
```
### Using Tools
The SDK supports function calling through tools:
```python
def weather_lookup(city_name: str) -> str:
return f"The weather in {city_name} is 22°C."
# Create a tools dictionary
tools = {
'weather_lookup': weather_lookup
}
# Use tools with the chat method
response_messages, state = client.chat(
messages=messages,
tools=tools
)
```
### Stateful Chat (Convenience Wrapper)
For simpler use cases, the SDK provides a `StatefulChat` class that maintains conversation state automatically:
```python
from rowboat import StatefulChat
# Initialize stateful chat
chat = StatefulChat(
client,
tools=tools,
system_prompt="You are a helpful assistant."
)
# Simply send messages and get responses
response = chat.run("Hello, how are you?")
print(response)
# I'm good, thanks! How can I help you today?
```
### Advanced Usage
#### Using a specific workflow
```python
response_messages, state = client.chat(
messages=messages,
workflow_id="<WORKFLOW_ID>"
)
# or
chat = StatefulChat(
client,
workflow_id="<WORKFLOW_ID>"
)
```
#### Using a test profile
You can specify a test profile ID to use a specific test configuration:
```python
response_messages, state = client.chat(
messages=messages,
test_profile_id="<TEST_PROFILE_ID>"
)
# or
chat = StatefulChat(
client,
test_profile_id="<TEST_PROFILE_ID>"
)
```
#### Skip tool call runs
This will surface the tool calls to the SDK instead of running them automatically on the Rowboat server.
```python
response_messages, state = client.chat(
messages=messages,
skip_tool_calls=True
)
# or
chat = StatefulChat(
client,
skip_tool_calls=True
)
response = client.chat(messages=messages, state=response.state)
```

View file

@ -4,9 +4,9 @@ build-backend = "hatchling.build"
[project]
name = "rowboat"
version = "2.1.0"
version = "3.0.0"
authors = [
{ name = "Your Name", email = "your.email@example.com" },
{ name = "Ramnique Singh", email = "ramnique@rowboatlabs.com" },
]
description = "Python sdk for the Rowboat API"
readme = "README.md"

View file

@ -1,18 +1,14 @@
from typing import Dict, List, Optional, Any, Callable, Union, Tuple
from typing import Dict, List, Optional, Any, Union
import requests
import json
from .schema import (
ApiRequest,
ApiResponse,
ApiMessage,
ToolMessage,
UserMessage,
SystemMessage,
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'
@ -25,16 +21,12 @@ class Client:
self,
messages: List[ApiMessage],
state: Optional[Dict[str, Any]] = None,
skip_tool_calls: bool = False,
max_turns: int = 3,
workflow_id: Optional[str] = None,
test_profile_id: Optional[str] = None
) -> ApiResponse:
request = ApiRequest(
messages=messages,
state=state,
skipToolCalls=skip_tool_calls,
maxTurns=max_turns,
workflowId=workflow_id,
testProfileId=test_profile_id
)
@ -55,86 +47,27 @@ class Client:
return response_data
def _process_tool_calls(
self,
tool_calls: List[Any],
tools: Dict[str, Callable[..., str]]
) -> List[ToolMessage]:
"""Process tool calls and return a list of tool response messages"""
tool_messages = []
for tool_call in tool_calls:
tool_name = tool_call.function.name
tool_arguments = json.loads(tool_call.function.arguments)
if tool_name not in tools:
raise ValueError(f'Missing tool: {tool_name}')
tool_response = tools[tool_name](**tool_arguments)
tool_msg = ToolMessage(
role='tool',
content=tool_response,
tool_call_id=tool_call.id,
tool_name=tool_name
)
tool_messages.append(tool_msg)
return tool_messages
def chat(
self,
messages: List[ApiMessage],
tools: Optional[Dict[str, Callable[..., str]]] = None,
state: Optional[Dict[str, Any]] = None,
max_turns: int = 3,
skip_tool_calls: bool = False,
workflow_id: Optional[str] = None,
test_profile_id: Optional[str] = None
) -> Tuple[List[ApiMessage], Optional[Dict[str, Any]]]:
"""Stateless chat method that handles a single conversation turn with multiple tool call rounds"""
) -> ApiResponse:
"""Stateless chat method that handles a single conversation turn"""
current_messages = messages[:]
current_state = state
turns = 0
# call api
response_data = self._call_api(
messages=messages,
state=state,
workflow_id=workflow_id,
test_profile_id=test_profile_id
)
response_messages = []
response_state = None
has_tool_calls = False
while turns < max_turns:
# call api
response_data = self._call_api(
messages=current_messages,
state=current_state,
skip_tool_calls=skip_tool_calls,
max_turns=max_turns,
workflow_id=workflow_id,
test_profile_id=test_profile_id
)
current_messages.extend(response_data.messages)
current_state = response_data.state
response_messages = response_data.messages
response_state = response_data.state
# Process tool calls if present and tools are provided
last_message = response_data.messages[-1]
has_tool_calls = hasattr(last_message, 'tool_calls') and last_message.tool_calls
if has_tool_calls:
tool_messages = self._process_tool_calls(last_message.tool_calls, tools)
current_messages.extend(tool_messages)
# If no tool calls were made, we're done
if not has_tool_calls:
break
turns += 1
if turns == max_turns and has_tool_calls:
raise ValueError("Max turns reached")
if not last_message.agenticResponseType == 'external':
if not response_data.messages[-1].agenticResponseType == 'external':
raise ValueError("Last message was not an external message")
return response_messages, response_state
return response_data
class StatefulChat:
"""Maintains conversation state across multiple turns"""
@ -142,23 +75,14 @@ class StatefulChat:
def __init__(
self,
client: Client,
tools: Optional[Dict[str, Callable[..., str]]] = None,
system_prompt: Optional[str] = None,
max_turns: int = 3,
skip_tool_calls: bool = False,
workflow_id: Optional[str] = None,
test_profile_id: Optional[str] = None
) -> None:
self.client = client
self.tools = tools
self.messages: List[ApiMessage] = []
self.state: Optional[Dict[str, Any]] = None
self.max_turns = max_turns
self.skip_tool_calls = skip_tool_calls
self.workflow_id = workflow_id
self.test_profile_id = test_profile_id
if system_prompt:
self.messages.append(SystemMessage(role='system', content=system_prompt))
def run(self, message: Union[str]) -> str:
"""Handle a single user turn in the conversation"""
@ -168,22 +92,19 @@ class StatefulChat:
self.messages.append(user_msg)
# Get response using the client's chat method
new_messages, new_state = self.client.chat(
response_data = self.client.chat(
messages=self.messages,
tools=self.tools,
state=self.state,
max_turns=self.max_turns,
skip_tool_calls=self.skip_tool_calls,
workflow_id=self.workflow_id,
test_profile_id=self.test_profile_id
)
# Update internal state
self.messages = new_messages
self.state = new_state
self.messages = response_data.messages
self.state = response_data.state
# Return only the final message content
last_message = new_messages[-1]
last_message = self.messages[-1]
return last_message.content
@ -197,9 +118,13 @@ if __name__ == "__main__":
api_key: str = "<API_KEY>"
client = Client(host, project_id, api_key)
tools: Dict[str, Callable[..., str]] = {
'weather_lookup': weather_lookup_tool
}
chat_session = StatefulChat(client, tools)
resp = chat_session.run("whats the weather in london?")
result = client.chat(
messages=[
UserMessage(role='user', content="Hello")
]
)
print(result.messages[-1].content)
chat_session = StatefulChat(client)
resp = chat_session.run("Hello")
print(resp)

View file

@ -48,8 +48,6 @@ ApiMessage = Union[
class ApiRequest(BaseModel):
messages: List[ApiMessage]
state: Any
skipToolCalls: Optional[bool] = None
maxTurns: Optional[int] = None
workflowId: Optional[str] = None
testProfileId: Optional[str] = None