mirror of https://github.com/kortix-ai/suna.git
- Enhanced storage capabilities for production readiness:
- Added SQLite as primary storage backend for ThreadManager and StateManager - Implemented persistent storage with unique store IDs - Added CRUD operations for state management - Enabled multiple concurrent stores with referential integrity - Improved state persistence and retrieval mechanisms
This commit is contained in:
parent
13e98678e8
commit
cb9f7b616f
|
@ -1,3 +1,11 @@
|
||||||
|
0.1.9
|
||||||
|
- Enhanced storage capabilities for production readiness:
|
||||||
|
- Added SQLite as primary storage backend for ThreadManager and StateManager
|
||||||
|
- Implemented persistent storage with unique store IDs
|
||||||
|
- Added CRUD operations for state management
|
||||||
|
- Enabled multiple concurrent stores with referential integrity
|
||||||
|
- Improved state persistence and retrieval mechanisms
|
||||||
|
|
||||||
0.1.8
|
0.1.8
|
||||||
- Added base processor classes for extensible tool handling:
|
- Added base processor classes for extensible tool handling:
|
||||||
- ToolParserBase: Abstract base class for parsing LLM responses
|
- ToolParserBase: Abstract base class for parsing LLM responses
|
||||||
|
|
Binary file not shown.
|
@ -91,12 +91,13 @@ file contents here
|
||||||
async def run_agent(thread_id: str, use_xml: bool = True, max_iterations: int = 5):
|
async def run_agent(thread_id: str, use_xml: bool = True, max_iterations: int = 5):
|
||||||
"""Run the development agent with specified configuration."""
|
"""Run the development agent with specified configuration."""
|
||||||
thread_manager = ThreadManager()
|
thread_manager = ThreadManager()
|
||||||
state_manager = StateManager()
|
|
||||||
|
|
||||||
thread_manager.add_tool(FilesTool)
|
store_id = await StateManager.create_store()
|
||||||
thread_manager.add_tool(TerminalTool)
|
state_manager = StateManager(store_id)
|
||||||
|
|
||||||
|
thread_manager.add_tool(FilesTool, store_id=store_id)
|
||||||
|
thread_manager.add_tool(TerminalTool, store_id=store_id)
|
||||||
|
|
||||||
# Combine base message with XML format if needed
|
|
||||||
system_message = {
|
system_message = {
|
||||||
"role": "system",
|
"role": "system",
|
||||||
"content": BASE_SYSTEM_MESSAGE + (XML_FORMAT if use_xml else "")
|
"content": BASE_SYSTEM_MESSAGE + (XML_FORMAT if use_xml else "")
|
||||||
|
@ -199,6 +200,7 @@ def main():
|
||||||
|
|
||||||
async def async_main():
|
async def async_main():
|
||||||
thread_manager = ThreadManager()
|
thread_manager = ThreadManager()
|
||||||
|
|
||||||
thread_id = await thread_manager.create_thread()
|
thread_id = await thread_manager.create_thread()
|
||||||
await thread_manager.add_message(
|
await thread_manager.add_message(
|
||||||
thread_id,
|
thread_id,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema
|
from agentpress.tools.tool import Tool, ToolResult, openapi_schema, xml_schema
|
||||||
from agentpress.state_manager import StateManager
|
from agentpress.state_manager import StateManager
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
class FilesTool(Tool):
|
class FilesTool(Tool):
|
||||||
"""File management tool for creating, updating, and deleting files.
|
"""File management tool for creating, updating, and deleting files.
|
||||||
|
@ -53,11 +54,11 @@ class FilesTool(Tool):
|
||||||
".sql"
|
".sql"
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, store_id: Optional[str] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.workspace = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'workspace')
|
self.workspace = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'workspace')
|
||||||
os.makedirs(self.workspace, exist_ok=True)
|
os.makedirs(self.workspace, exist_ok=True)
|
||||||
self.state_manager = StateManager("state.json")
|
self.state_manager = StateManager(store_id)
|
||||||
self.SNIPPET_LINES = 4 # Number of context lines to show around edits
|
self.SNIPPET_LINES = 4 # Number of context lines to show around edits
|
||||||
asyncio.create_task(self._init_workspace_state())
|
asyncio.create_task(self._init_workspace_state())
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,16 @@ import asyncio
|
||||||
import subprocess
|
import subprocess
|
||||||
from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema
|
from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema
|
||||||
from agentpress.state_manager import StateManager
|
from agentpress.state_manager import StateManager
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
class TerminalTool(Tool):
|
class TerminalTool(Tool):
|
||||||
"""Terminal command execution tool for workspace operations."""
|
"""Terminal command execution tool for workspace operations."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, store_id: Optional[str] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.workspace = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'workspace')
|
self.workspace = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'workspace')
|
||||||
os.makedirs(self.workspace, exist_ok=True)
|
os.makedirs(self.workspace, exist_ok=True)
|
||||||
self.state_manager = StateManager("state.json")
|
self.state_manager = StateManager(store_id)
|
||||||
|
|
||||||
async def _update_command_history(self, command: str, output: str, success: bool):
|
async def _update_command_history(self, command: str, output: str, success: bool):
|
||||||
"""Update command history in state"""
|
"""Update command history in state"""
|
||||||
|
|
|
@ -26,14 +26,14 @@ MODULES = {
|
||||||
"processors": {
|
"processors": {
|
||||||
"required": True,
|
"required": True,
|
||||||
"files": [
|
"files": [
|
||||||
"base_processors.py",
|
"processor/base_processors.py",
|
||||||
"llm_response_processor.py",
|
"processor/llm_response_processor.py",
|
||||||
"standard_tool_parser.py",
|
"processor/standard/standard_tool_parser.py",
|
||||||
"standard_tool_executor.py",
|
"processor/standard/standard_tool_executor.py",
|
||||||
"standard_results_adder.py",
|
"processor/standard/standard_results_adder.py",
|
||||||
"xml_tool_parser.py",
|
"processor/xml/xml_tool_parser.py",
|
||||||
"xml_tool_executor.py",
|
"processor/xml/xml_tool_executor.py",
|
||||||
"xml_results_adder.py"
|
"processor/xml/xml_results_adder.py"
|
||||||
],
|
],
|
||||||
"description": "Response Processing System - Handles parsing and executing LLM responses, managing tool calls, and processing results. Supports both standard OpenAI-style function calling and XML-based tool execution patterns."
|
"description": "Response Processing System - Handles parsing and executing LLM responses, managing tool calls, and processing results. Supports both standard OpenAI-style function calling and XML-based tool execution patterns."
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
"""
|
||||||
|
Centralized database connection management for AgentPress.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import aiosqlite
|
||||||
|
import logging
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
class DBConnection:
|
||||||
|
"""Singleton database connection manager."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
_initialized = False
|
||||||
|
_db_path = os.path.join(os.getcwd(), "agentpress.db")
|
||||||
|
_init_lock = asyncio.Lock()
|
||||||
|
_initialization_task = None
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
# Start initialization when instance is first created
|
||||||
|
cls._initialization_task = asyncio.create_task(cls._instance._initialize())
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""No initialization needed in __init__ as it's handled in __new__"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _initialize(cls):
|
||||||
|
"""Internal initialization method."""
|
||||||
|
if cls._initialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
async with cls._init_lock:
|
||||||
|
if cls._initialized: # Double-check after acquiring lock
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiosqlite.connect(cls._db_path) as db:
|
||||||
|
# Threads table
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS threads (
|
||||||
|
thread_id TEXT PRIMARY KEY,
|
||||||
|
messages TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# State stores table
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS state_stores (
|
||||||
|
store_id TEXT PRIMARY KEY,
|
||||||
|
store_data TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
cls._initialized = True
|
||||||
|
logging.info("Database schema initialized")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Database initialization error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_db_path(cls, db_path: str):
|
||||||
|
"""Set custom database path."""
|
||||||
|
if cls._initialized:
|
||||||
|
raise RuntimeError("Cannot change database path after initialization")
|
||||||
|
cls._db_path = db_path
|
||||||
|
logging.info(f"Updated database path to: {db_path}")
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def connection(self):
|
||||||
|
"""Get a database connection."""
|
||||||
|
# Wait for initialization to complete if it hasn't already
|
||||||
|
if self._initialization_task and not self._initialized:
|
||||||
|
await self._initialization_task
|
||||||
|
|
||||||
|
async with aiosqlite.connect(self._db_path) as conn:
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Database error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def transaction(self):
|
||||||
|
"""Execute operations in a transaction."""
|
||||||
|
async with self.connection() as db:
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
await db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
await db.rollback()
|
||||||
|
logging.error(f"Transaction error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def execute(self, query: str, params: tuple = ()):
|
||||||
|
"""Execute a single query."""
|
||||||
|
async with self.connection() as db:
|
||||||
|
try:
|
||||||
|
result = await db.execute(query, params)
|
||||||
|
await db.commit()
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Query execution error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def fetch_one(self, query: str, params: tuple = ()):
|
||||||
|
"""Fetch a single row."""
|
||||||
|
async with self.connection() as db:
|
||||||
|
async with db.execute(query, params) as cursor:
|
||||||
|
return await cursor.fetchone()
|
||||||
|
|
||||||
|
async def fetch_all(self, query: str, params: tuple = ()):
|
||||||
|
"""Fetch all rows."""
|
||||||
|
async with self.connection() as db:
|
||||||
|
async with db.execute(query, params) as cursor:
|
||||||
|
return await cursor.fetchall()
|
|
@ -172,7 +172,7 @@ class ResultsAdderBase(ABC):
|
||||||
Attributes:
|
Attributes:
|
||||||
add_message: Callback for adding new messages
|
add_message: Callback for adding new messages
|
||||||
update_message: Callback for updating existing messages
|
update_message: Callback for updating existing messages
|
||||||
list_messages: Callback for retrieving thread messages
|
get_messages: Callback for retrieving thread messages
|
||||||
message_added: Flag tracking if initial message has been added
|
message_added: Flag tracking if initial message has been added
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@ class ResultsAdderBase(ABC):
|
||||||
"""
|
"""
|
||||||
self.add_message = thread_manager.add_message
|
self.add_message = thread_manager.add_message
|
||||||
self.update_message = thread_manager._update_message
|
self.update_message = thread_manager._update_message
|
||||||
self.list_messages = thread_manager.list_messages
|
self.get_messages = thread_manager.get_messages
|
||||||
self.message_added = False
|
self.message_added = False
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
|
@ -11,10 +11,10 @@ This module provides comprehensive processing of LLM responses, including:
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Callable, Dict, Any, AsyncGenerator, Optional
|
from typing import Callable, Dict, Any, AsyncGenerator, Optional
|
||||||
import logging
|
import logging
|
||||||
from agentpress.base_processors import ToolParserBase, ToolExecutorBase, ResultsAdderBase
|
from agentpress.processor.base_processors import ToolParserBase, ToolExecutorBase, ResultsAdderBase
|
||||||
from agentpress.standard_tool_parser import StandardToolParser
|
from agentpress.processor.standard.standard_tool_parser import StandardToolParser
|
||||||
from agentpress.standard_tool_executor import StandardToolExecutor
|
from agentpress.processor.standard.standard_tool_executor import StandardToolExecutor
|
||||||
from agentpress.standard_results_adder import StandardResultsAdder
|
from agentpress.processor.standard.standard_results_adder import StandardResultsAdder
|
||||||
|
|
||||||
class LLMResponseProcessor:
|
class LLMResponseProcessor:
|
||||||
"""Handles LLM response processing and tool execution management.
|
"""Handles LLM response processing and tool execution management.
|
||||||
|
@ -40,9 +40,8 @@ class LLMResponseProcessor:
|
||||||
available_functions: Dict = None,
|
available_functions: Dict = None,
|
||||||
add_message_callback: Callable = None,
|
add_message_callback: Callable = None,
|
||||||
update_message_callback: Callable = None,
|
update_message_callback: Callable = None,
|
||||||
list_messages_callback: Callable = None,
|
get_messages_callback: Callable = None,
|
||||||
parallel_tool_execution: bool = True,
|
parallel_tool_execution: bool = True,
|
||||||
threads_dir: str = "threads",
|
|
||||||
tool_parser: Optional[ToolParserBase] = None,
|
tool_parser: Optional[ToolParserBase] = None,
|
||||||
tool_executor: Optional[ToolExecutorBase] = None,
|
tool_executor: Optional[ToolExecutorBase] = None,
|
||||||
results_adder: Optional[ResultsAdderBase] = None,
|
results_adder: Optional[ResultsAdderBase] = None,
|
||||||
|
@ -55,9 +54,8 @@ class LLMResponseProcessor:
|
||||||
available_functions: Dictionary of available tool functions
|
available_functions: Dictionary of available tool functions
|
||||||
add_message_callback: Callback for adding messages
|
add_message_callback: Callback for adding messages
|
||||||
update_message_callback: Callback for updating messages
|
update_message_callback: Callback for updating messages
|
||||||
list_messages_callback: Callback for listing messages
|
get_messages_callback: Callback for listing messages
|
||||||
parallel_tool_execution: Whether to execute tools in parallel
|
parallel_tool_execution: Whether to execute tools in parallel
|
||||||
threads_dir: Directory for thread storage
|
|
||||||
tool_parser: Custom tool parser implementation
|
tool_parser: Custom tool parser implementation
|
||||||
tool_executor: Custom tool executor implementation
|
tool_executor: Custom tool executor implementation
|
||||||
results_adder: Custom results adder implementation
|
results_adder: Custom results adder implementation
|
||||||
|
@ -67,16 +65,15 @@ class LLMResponseProcessor:
|
||||||
self.tool_executor = tool_executor or StandardToolExecutor(parallel=parallel_tool_execution)
|
self.tool_executor = tool_executor or StandardToolExecutor(parallel=parallel_tool_execution)
|
||||||
self.tool_parser = tool_parser or StandardToolParser()
|
self.tool_parser = tool_parser or StandardToolParser()
|
||||||
self.available_functions = available_functions or {}
|
self.available_functions = available_functions or {}
|
||||||
self.threads_dir = threads_dir
|
|
||||||
|
|
||||||
# Create minimal thread manager if needed
|
# Create minimal thread manager if needed
|
||||||
if thread_manager is None and (add_message_callback and update_message_callback and list_messages_callback):
|
if thread_manager is None and (add_message_callback and update_message_callback and get_messages_callback):
|
||||||
class MinimalThreadManager:
|
class MinimalThreadManager:
|
||||||
def __init__(self, add_msg, update_msg, list_msg):
|
def __init__(self, add_msg, update_msg, list_msg):
|
||||||
self.add_message = add_msg
|
self.add_message = add_msg
|
||||||
self._update_message = update_msg
|
self._update_message = update_msg
|
||||||
self.list_messages = list_msg
|
self.get_messages = list_msg
|
||||||
thread_manager = MinimalThreadManager(add_message_callback, update_message_callback, list_messages_callback)
|
thread_manager = MinimalThreadManager(add_message_callback, update_message_callback, get_messages_callback)
|
||||||
|
|
||||||
self.results_adder = results_adder or StandardResultsAdder(thread_manager)
|
self.results_adder = results_adder or StandardResultsAdder(thread_manager)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from agentpress.base_processors import ResultsAdderBase
|
from agentpress.processor.base_processors import ResultsAdderBase
|
||||||
|
|
||||||
# --- Standard Results Adder Implementation ---
|
# --- Standard Results Adder Implementation ---
|
||||||
|
|
||||||
|
@ -81,6 +81,6 @@ class StandardResultsAdder(ResultsAdderBase):
|
||||||
- Checks for duplicate tool results before adding
|
- Checks for duplicate tool results before adding
|
||||||
- Adds result only if tool_call_id is unique
|
- Adds result only if tool_call_id is unique
|
||||||
"""
|
"""
|
||||||
messages = await self.list_messages(thread_id)
|
messages = await self.get_messages(thread_id)
|
||||||
if not any(msg.get('tool_call_id') == result['tool_call_id'] for msg in messages):
|
if not any(msg.get('tool_call_id') == result['tool_call_id'] for msg in messages):
|
||||||
await self.add_message(thread_id, result)
|
await self.add_message(thread_id, result)
|
|
@ -9,7 +9,7 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, List, Set, Callable, Optional
|
from typing import Dict, Any, List, Set, Callable, Optional
|
||||||
from agentpress.base_processors import ToolExecutorBase
|
from agentpress.processor.base_processors import ToolExecutorBase
|
||||||
from agentpress.tool import ToolResult
|
from agentpress.tool import ToolResult
|
||||||
|
|
||||||
# --- Standard Tool Executor Implementation ---
|
# --- Standard Tool Executor Implementation ---
|
|
@ -1,6 +1,6 @@
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
from agentpress.base_processors import ToolParserBase
|
from agentpress.processor.base_processors import ToolParserBase
|
||||||
|
|
||||||
# --- Standard Tool Parser Implementation ---
|
# --- Standard Tool Parser Implementation ---
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from agentpress.base_processors import ResultsAdderBase
|
from agentpress.processor.base_processors import ResultsAdderBase
|
||||||
|
|
||||||
class XMLResultsAdder(ResultsAdderBase):
|
class XMLResultsAdder(ResultsAdderBase):
|
||||||
"""XML-specific implementation for handling tool results and message processing.
|
"""XML-specific implementation for handling tool results and message processing.
|
||||||
|
@ -79,7 +79,7 @@ class XMLResultsAdder(ResultsAdderBase):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get the original tool call to find the root tag
|
# Get the original tool call to find the root tag
|
||||||
messages = await self.list_messages(thread_id)
|
messages = await self.get_messages(thread_id)
|
||||||
assistant_msg = next((msg for msg in reversed(messages)
|
assistant_msg = next((msg for msg in reversed(messages)
|
||||||
if msg['role'] == 'assistant'), None)
|
if msg['role'] == 'assistant'), None)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from typing import List, Dict, Any, Set, Callable, Optional
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from agentpress.base_processors import ToolExecutorBase
|
from agentpress.processor.base_processors import ToolExecutorBase
|
||||||
from agentpress.tool import ToolResult
|
from agentpress.tool import ToolResult
|
||||||
from agentpress.tool_registry import ToolRegistry
|
from agentpress.tool_registry import ToolRegistry
|
||||||
|
|
|
@ -7,7 +7,7 @@ complete and streaming responses with robust XML parsing and validation capabili
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, Optional, List, Tuple
|
from typing import Dict, Any, Optional, List, Tuple
|
||||||
from agentpress.base_processors import ToolParserBase
|
from agentpress.processor.base_processors import ToolParserBase
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from agentpress.tool_registry import ToolRegistry
|
from agentpress.tool_registry import ToolRegistry
|
|
@ -1,76 +1,100 @@
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, Optional, List, Dict, Union, AsyncGenerator
|
||||||
from asyncio import Lock
|
from asyncio import Lock
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
import uuid
|
||||||
|
from agentpress.db_connection import DBConnection
|
||||||
|
import asyncio
|
||||||
|
|
||||||
class StateManager:
|
class StateManager:
|
||||||
"""
|
"""
|
||||||
Manages persistent state storage for AgentPress components.
|
Manages persistent state storage for AgentPress components.
|
||||||
|
|
||||||
The StateManager provides thread-safe access to a JSON-based state store,
|
The StateManager provides thread-safe access to a SQLite-based state store,
|
||||||
allowing components to save and retrieve data across sessions. It handles
|
allowing components to save and retrieve data across sessions. Each store
|
||||||
concurrent access using asyncio locks and provides atomic operations for
|
has a unique ID and contains multiple key-value pairs in a single JSON object.
|
||||||
state modifications.
|
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
lock (Lock): Asyncio lock for thread-safe state access
|
lock (Lock): Asyncio lock for thread-safe state access
|
||||||
store_file (str): Path to the JSON file storing the state
|
db (DBConnection): Database connection manager
|
||||||
|
store_id (str): Unique identifier for this state store
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, store_file: str = "state.json"):
|
def __init__(self, store_id: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
Initialize StateManager with custom store file name.
|
Initialize StateManager with optional store ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
store_file (str): Path to the JSON file to store state.
|
store_id (str, optional): Unique identifier for the store. If None, creates new.
|
||||||
Defaults to "state.json" in the current directory.
|
|
||||||
"""
|
"""
|
||||||
self.lock = Lock()
|
self.lock = Lock()
|
||||||
self.store_file = store_file
|
self.db = DBConnection()
|
||||||
logging.info(f"StateManager initialized with store file: {store_file}")
|
self.store_id = store_id or str(uuid.uuid4())
|
||||||
|
logging.info(f"StateManager initialized with store_id: {self.store_id}")
|
||||||
|
asyncio.create_task(self._ensure_store_exists())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create_store(cls) -> str:
|
||||||
|
"""Create a new state store and return its ID."""
|
||||||
|
store_id = str(uuid.uuid4())
|
||||||
|
manager = cls(store_id)
|
||||||
|
await manager._ensure_store_exists()
|
||||||
|
return store_id
|
||||||
|
|
||||||
|
async def _ensure_store_exists(self):
|
||||||
|
"""Ensure store exists in database."""
|
||||||
|
async with self.db.transaction() as conn:
|
||||||
|
await conn.execute("""
|
||||||
|
INSERT OR IGNORE INTO state_stores (store_id, store_data)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""", (self.store_id, json.dumps({})))
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def store_scope(self):
|
async def store_scope(self):
|
||||||
"""
|
"""
|
||||||
Context manager for atomic state operations.
|
Context manager for atomic state operations.
|
||||||
|
|
||||||
Provides thread-safe access to the state store, handling file I/O
|
Provides thread-safe access to the state store, handling database
|
||||||
and ensuring proper cleanup. Automatically loads the current state
|
operations and ensuring proper cleanup.
|
||||||
and saves changes when the context exits.
|
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
dict: The current state store contents
|
dict: The current state store contents
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Exception: If there are errors reading from or writing to the store file
|
Exception: If there are errors with database operations
|
||||||
"""
|
"""
|
||||||
|
async with self.lock:
|
||||||
try:
|
try:
|
||||||
# Read current state
|
async with self.db.transaction() as conn:
|
||||||
if os.path.exists(self.store_file):
|
async with conn.execute(
|
||||||
with open(self.store_file, 'r') as f:
|
"SELECT store_data FROM state_stores WHERE store_id = ?",
|
||||||
store = json.load(f)
|
(self.store_id,)
|
||||||
else:
|
) as cursor:
|
||||||
store = {}
|
row = await cursor.fetchone()
|
||||||
|
store = json.loads(row[0]) if row else {}
|
||||||
|
|
||||||
yield store
|
yield store
|
||||||
|
|
||||||
# Write updated state
|
await conn.execute(
|
||||||
with open(self.store_file, 'w') as f:
|
"""
|
||||||
json.dump(store, f, indent=2)
|
UPDATE state_stores
|
||||||
logging.debug("Store saved successfully")
|
SET store_data = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE store_id = ?
|
||||||
|
""",
|
||||||
|
(json.dumps(store), self.store_id)
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Error in store operation", exc_info=True)
|
logging.error("Error in store operation", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def set(self, key: str, data: Any):
|
async def set(self, key: str, data: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
Store any JSON-serializable data with a simple key.
|
Store any JSON-serializable data with a key.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): Simple string key like "config" or "settings"
|
key (str): Simple string key like "config" or "settings"
|
||||||
data (Any): Any JSON-serializable data (dict, list, str, int, bool, etc)
|
data (Any): Any JSON-serializable data
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: The stored data
|
Any: The stored data
|
||||||
|
@ -78,17 +102,12 @@ class StateManager:
|
||||||
Raises:
|
Raises:
|
||||||
Exception: If there are errors during storage operation
|
Exception: If there are errors during storage operation
|
||||||
"""
|
"""
|
||||||
async with self.lock:
|
|
||||||
async with self.store_scope() as store:
|
async with self.store_scope() as store:
|
||||||
try:
|
store[key] = data
|
||||||
store[key] = data # Will be JSON serialized when written to file
|
|
||||||
logging.info(f'Updated store key: {key}')
|
logging.info(f'Updated store key: {key}')
|
||||||
return data
|
return data
|
||||||
except Exception as e:
|
|
||||||
logging.error(f'Error in set: {str(e)}')
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def get(self, key: str) -> Any:
|
async def get(self, key: str) -> Optional[Any]:
|
||||||
"""
|
"""
|
||||||
Get data for a key.
|
Get data for a key.
|
||||||
|
|
||||||
|
@ -97,9 +116,6 @@ class StateManager:
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Any: The stored data for the key, or None if key not found
|
Any: The stored data for the key, or None if key not found
|
||||||
|
|
||||||
Note:
|
|
||||||
This operation is read-only and doesn't require locking
|
|
||||||
"""
|
"""
|
||||||
async with self.store_scope() as store:
|
async with self.store_scope() as store:
|
||||||
if key in store:
|
if key in store:
|
||||||
|
@ -115,17 +131,31 @@ class StateManager:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): Simple string key like "config" or "settings"
|
key (str): Simple string key like "config" or "settings"
|
||||||
|
|
||||||
Note:
|
|
||||||
No error is raised if the key doesn't exist
|
|
||||||
"""
|
"""
|
||||||
async with self.lock:
|
|
||||||
async with self.store_scope() as store:
|
async with self.store_scope() as store:
|
||||||
if key in store:
|
if key in store:
|
||||||
del store[key]
|
del store[key]
|
||||||
logging.info(f"Deleted key: {key}")
|
logging.info(f"Deleted key: {key}")
|
||||||
else:
|
|
||||||
logging.info(f"Key not found for deletion: {key}")
|
async def update(self, key: str, data: Dict[str, Any]) -> Optional[Any]:
|
||||||
|
"""Update existing data for a key by merging dictionaries."""
|
||||||
|
async with self.store_scope() as store:
|
||||||
|
if key in store and isinstance(store[key], dict):
|
||||||
|
store[key].update(data)
|
||||||
|
logging.info(f'Updated store key: {key}')
|
||||||
|
return store[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def append(self, key: str, item: Any) -> Optional[List[Any]]:
|
||||||
|
"""Append an item to a list stored at key."""
|
||||||
|
async with self.store_scope() as store:
|
||||||
|
if key not in store:
|
||||||
|
store[key] = []
|
||||||
|
if isinstance(store[key], list):
|
||||||
|
store[key].append(item)
|
||||||
|
logging.info(f'Appended to key: {key}')
|
||||||
|
return store[key]
|
||||||
|
return None
|
||||||
|
|
||||||
async def export_store(self) -> dict:
|
async def export_store(self) -> dict:
|
||||||
"""
|
"""
|
||||||
|
@ -133,9 +163,6 @@ class StateManager:
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Complete contents of the state store
|
dict: Complete contents of the state store
|
||||||
|
|
||||||
Note:
|
|
||||||
This operation is read-only and returns a copy of the store
|
|
||||||
"""
|
"""
|
||||||
async with self.store_scope() as store:
|
async with self.store_scope() as store:
|
||||||
logging.info(f"Store content: {store}")
|
logging.info(f"Store content: {store}")
|
||||||
|
@ -148,7 +175,29 @@ class StateManager:
|
||||||
Removes all data from the store, resetting it to an empty state.
|
Removes all data from the store, resetting it to an empty state.
|
||||||
This operation is atomic and thread-safe.
|
This operation is atomic and thread-safe.
|
||||||
"""
|
"""
|
||||||
async with self.lock:
|
|
||||||
async with self.store_scope() as store:
|
async with self.store_scope() as store:
|
||||||
store.clear()
|
store.clear()
|
||||||
logging.info("Cleared store")
|
logging.info("Cleared store")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def list_stores(cls) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
List all available state stores.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of store information including IDs and timestamps
|
||||||
|
"""
|
||||||
|
db = DBConnection()
|
||||||
|
async with db.transaction() as conn:
|
||||||
|
async with conn.execute(
|
||||||
|
"SELECT store_id, created_at, updated_at FROM state_stores ORDER BY updated_at DESC"
|
||||||
|
) as cursor:
|
||||||
|
stores = [
|
||||||
|
{
|
||||||
|
"store_id": row[0],
|
||||||
|
"created_at": row[1],
|
||||||
|
"updated_at": row[2]
|
||||||
|
}
|
||||||
|
for row in await cursor.fetchall()
|
||||||
|
]
|
||||||
|
return stores
|
||||||
|
|
|
@ -11,21 +11,22 @@ This module provides comprehensive conversation management, including:
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import asyncio
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List, Dict, Any, Optional, Type, Union, AsyncGenerator
|
from typing import List, Dict, Any, Optional, Type, Union, AsyncGenerator
|
||||||
from agentpress.llm import make_llm_api_call
|
from agentpress.llm import make_llm_api_call
|
||||||
from agentpress.tool import Tool, ToolResult
|
from agentpress.tool import Tool, ToolResult
|
||||||
from agentpress.tool_registry import ToolRegistry
|
from agentpress.tool_registry import ToolRegistry
|
||||||
from agentpress.llm_response_processor import LLMResponseProcessor
|
from agentpress.processor.llm_response_processor import LLMResponseProcessor
|
||||||
from agentpress.base_processors import ToolParserBase, ToolExecutorBase, ResultsAdderBase
|
from agentpress.processor.base_processors import ToolParserBase, ToolExecutorBase, ResultsAdderBase
|
||||||
|
from agentpress.db_connection import DBConnection
|
||||||
|
|
||||||
from agentpress.xml_tool_parser import XMLToolParser
|
from agentpress.processor.xml.xml_tool_parser import XMLToolParser
|
||||||
from agentpress.xml_tool_executor import XMLToolExecutor
|
from agentpress.processor.xml.xml_tool_executor import XMLToolExecutor
|
||||||
from agentpress.xml_results_adder import XMLResultsAdder
|
from agentpress.processor.xml.xml_results_adder import XMLResultsAdder
|
||||||
from agentpress.standard_tool_parser import StandardToolParser
|
from agentpress.processor.standard.standard_tool_parser import StandardToolParser
|
||||||
from agentpress.standard_tool_executor import StandardToolExecutor
|
from agentpress.processor.standard.standard_tool_executor import StandardToolExecutor
|
||||||
from agentpress.standard_results_adder import StandardResultsAdder
|
from agentpress.processor.standard.standard_results_adder import StandardResultsAdder
|
||||||
|
|
||||||
class ThreadManager:
|
class ThreadManager:
|
||||||
"""Manages conversation threads with LLM models and tool execution.
|
"""Manages conversation threads with LLM models and tool execution.
|
||||||
|
@ -33,92 +34,35 @@ class ThreadManager:
|
||||||
Provides comprehensive conversation management, handling message threading,
|
Provides comprehensive conversation management, handling message threading,
|
||||||
tool registration, and LLM interactions with support for both standard and
|
tool registration, and LLM interactions with support for both standard and
|
||||||
XML-based tool execution patterns.
|
XML-based tool execution patterns.
|
||||||
|
|
||||||
Attributes:
|
|
||||||
threads_dir (str): Directory for storing thread files
|
|
||||||
tool_registry (ToolRegistry): Registry for managing available tools
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
add_tool: Register a tool with optional function filtering
|
|
||||||
create_thread: Create a new conversation thread
|
|
||||||
add_message: Add a message to a thread
|
|
||||||
list_messages: Retrieve messages from a thread
|
|
||||||
run_thread: Execute a conversation thread with LLM
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, threads_dir: str = "threads"):
|
def __init__(self):
|
||||||
"""Initialize ThreadManager.
|
"""Initialize ThreadManager."""
|
||||||
|
self.db = DBConnection()
|
||||||
Args:
|
|
||||||
threads_dir: Directory to store thread files
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
Creates the threads directory if it doesn't exist
|
|
||||||
"""
|
|
||||||
self.threads_dir = threads_dir
|
|
||||||
self.tool_registry = ToolRegistry()
|
self.tool_registry = ToolRegistry()
|
||||||
os.makedirs(self.threads_dir, exist_ok=True)
|
|
||||||
|
|
||||||
def add_tool(self, tool_class: Type[Tool], function_names: Optional[List[str]] = None, **kwargs):
|
def add_tool(self, tool_class: Type[Tool], function_names: Optional[List[str]] = None, **kwargs):
|
||||||
"""Add a tool to the ThreadManager.
|
"""Add a tool to the ThreadManager."""
|
||||||
|
|
||||||
Args:
|
|
||||||
tool_class: The tool class to register
|
|
||||||
function_names: Optional list of specific functions to register
|
|
||||||
**kwargs: Additional arguments passed to tool initialization
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- If function_names is None, all functions are registered
|
|
||||||
- Tool instances are created with provided kwargs
|
|
||||||
"""
|
|
||||||
self.tool_registry.register_tool(tool_class, function_names, **kwargs)
|
self.tool_registry.register_tool(tool_class, function_names, **kwargs)
|
||||||
|
|
||||||
async def create_thread(self) -> str:
|
async def create_thread(self) -> str:
|
||||||
"""Create a new conversation thread.
|
"""Create a new conversation thread."""
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Unique thread ID for the created thread
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
IOError: If thread file creation fails
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
Creates a new thread file with an empty messages list
|
|
||||||
"""
|
|
||||||
thread_id = str(uuid.uuid4())
|
thread_id = str(uuid.uuid4())
|
||||||
thread_path = os.path.join(self.threads_dir, f"{thread_id}.json")
|
await self.db.execute(
|
||||||
with open(thread_path, 'w') as f:
|
"INSERT INTO threads (thread_id, messages) VALUES (?, ?)",
|
||||||
json.dump({"messages": []}, f)
|
(thread_id, json.dumps([]))
|
||||||
|
)
|
||||||
return thread_id
|
return thread_id
|
||||||
|
|
||||||
async def add_message(self, thread_id: str, message_data: Dict[str, Any], images: Optional[List[Dict[str, Any]]] = None):
|
async def add_message(self, thread_id: str, message_data: Dict[str, Any], images: Optional[List[Dict[str, Any]]] = None):
|
||||||
"""Add a message to an existing thread.
|
"""Add a message to an existing thread."""
|
||||||
|
|
||||||
Args:
|
|
||||||
thread_id: ID of the target thread
|
|
||||||
message_data: Message content and metadata
|
|
||||||
images: Optional list of image data dictionaries
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
FileNotFoundError: If thread doesn't exist
|
|
||||||
Exception: For other operation failures
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Handles cleanup of incomplete tool calls
|
|
||||||
- Supports both text and image content
|
|
||||||
- Converts ToolResult instances to strings
|
|
||||||
"""
|
|
||||||
logging.info(f"Adding message to thread {thread_id} with images: {images}")
|
logging.info(f"Adding message to thread {thread_id} with images: {images}")
|
||||||
thread_path = os.path.join(self.threads_dir, f"{thread_id}.json")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(thread_path, 'r') as f:
|
async with self.db.transaction() as conn:
|
||||||
thread_data = json.load(f)
|
|
||||||
|
|
||||||
messages = thread_data["messages"]
|
|
||||||
|
|
||||||
# Handle cleanup of incomplete tool calls
|
# Handle cleanup of incomplete tool calls
|
||||||
if message_data['role'] == 'user':
|
if message_data['role'] == 'user':
|
||||||
|
messages = await self.get_messages(thread_id)
|
||||||
last_assistant_index = next((i for i in reversed(range(len(messages)))
|
last_assistant_index = next((i for i in reversed(range(len(messages)))
|
||||||
if messages[i]['role'] == 'assistant' and 'tool_calls' in messages[i]), None)
|
if messages[i]['role'] == 'assistant' and 'tool_calls' in messages[i]), None)
|
||||||
|
|
||||||
|
@ -152,45 +96,49 @@ class ThreadManager:
|
||||||
}
|
}
|
||||||
message_data['content'].append(image_content)
|
message_data['content'].append(image_content)
|
||||||
|
|
||||||
messages.append(message_data)
|
# Get current messages
|
||||||
thread_data["messages"] = messages
|
row = await self.db.fetch_one(
|
||||||
|
"SELECT messages FROM threads WHERE thread_id = ?",
|
||||||
|
(thread_id,)
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
raise ValueError(f"Thread {thread_id} not found")
|
||||||
|
|
||||||
with open(thread_path, 'w') as f:
|
messages = json.loads(row[0])
|
||||||
json.dump(thread_data, f)
|
messages.append(message_data)
|
||||||
|
|
||||||
|
# Update thread
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE threads
|
||||||
|
SET messages = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE thread_id = ?
|
||||||
|
""",
|
||||||
|
(json.dumps(messages), thread_id)
|
||||||
|
)
|
||||||
|
|
||||||
logging.info(f"Message added to thread {thread_id}: {message_data}")
|
logging.info(f"Message added to thread {thread_id}: {message_data}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to add message to thread {thread_id}: {e}")
|
logging.error(f"Failed to add message to thread {thread_id}: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def list_messages(
|
async def get_messages(
|
||||||
self,
|
self,
|
||||||
thread_id: str,
|
thread_id: str,
|
||||||
hide_tool_msgs: bool = False,
|
hide_tool_msgs: bool = False,
|
||||||
only_latest_assistant: bool = False,
|
only_latest_assistant: bool = False,
|
||||||
regular_list: bool = True
|
regular_list: bool = True
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""Retrieve messages from a thread with optional filtering.
|
"""Retrieve messages from a thread with optional filtering."""
|
||||||
|
row = await self.db.fetch_one(
|
||||||
|
"SELECT messages FROM threads WHERE thread_id = ?",
|
||||||
|
(thread_id,)
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
return []
|
||||||
|
|
||||||
Args:
|
messages = json.loads(row[0])
|
||||||
thread_id: ID of the thread to retrieve messages from
|
|
||||||
hide_tool_msgs: If True, excludes tool messages and tool calls
|
|
||||||
only_latest_assistant: If True, returns only the most recent assistant message
|
|
||||||
regular_list: If True, only includes standard message types
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of messages matching the filter criteria
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Returns empty list if thread doesn't exist
|
|
||||||
- Filters can be combined for different views of the conversation
|
|
||||||
"""
|
|
||||||
thread_path = os.path.join(self.threads_dir, f"{thread_id}.json")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(thread_path, 'r') as f:
|
|
||||||
thread_data = json.load(f)
|
|
||||||
messages = thread_data["messages"]
|
|
||||||
|
|
||||||
if only_latest_assistant:
|
if only_latest_assistant:
|
||||||
for msg in reversed(messages):
|
for msg in reversed(messages):
|
||||||
|
@ -198,39 +146,51 @@ class ThreadManager:
|
||||||
return [msg]
|
return [msg]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
filtered_messages = messages
|
|
||||||
|
|
||||||
if hide_tool_msgs:
|
if hide_tool_msgs:
|
||||||
filtered_messages = [
|
messages = [
|
||||||
{k: v for k, v in msg.items() if k != 'tool_calls'}
|
{k: v for k, v in msg.items() if k != 'tool_calls'}
|
||||||
for msg in filtered_messages
|
for msg in messages
|
||||||
if msg.get('role') != 'tool'
|
if msg.get('role') != 'tool'
|
||||||
]
|
]
|
||||||
|
|
||||||
if regular_list:
|
if regular_list:
|
||||||
filtered_messages = [
|
messages = [
|
||||||
msg for msg in filtered_messages
|
msg for msg in messages
|
||||||
if msg.get('role') in ['system', 'assistant', 'tool', 'user']
|
if msg.get('role') in ['system', 'assistant', 'tool', 'user']
|
||||||
]
|
]
|
||||||
|
|
||||||
return filtered_messages
|
return messages
|
||||||
except FileNotFoundError:
|
|
||||||
return []
|
async def _update_message(self, thread_id: str, message: Dict[str, Any]):
|
||||||
|
"""Update an existing message in the thread."""
|
||||||
|
async with self.db.transaction() as conn:
|
||||||
|
row = await self.db.fetch_one(
|
||||||
|
"SELECT messages FROM threads WHERE thread_id = ?",
|
||||||
|
(thread_id,)
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
return
|
||||||
|
|
||||||
|
messages = json.loads(row[0])
|
||||||
|
|
||||||
|
# Find and update the last assistant message
|
||||||
|
for i in reversed(range(len(messages))):
|
||||||
|
if messages[i].get('role') == 'assistant':
|
||||||
|
messages[i] = message
|
||||||
|
break
|
||||||
|
|
||||||
|
await conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE threads
|
||||||
|
SET messages = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE thread_id = ?
|
||||||
|
""",
|
||||||
|
(json.dumps(messages), thread_id)
|
||||||
|
)
|
||||||
|
|
||||||
async def cleanup_incomplete_tool_calls(self, thread_id: str):
|
async def cleanup_incomplete_tool_calls(self, thread_id: str):
|
||||||
"""Clean up incomplete tool calls in a thread.
|
"""Clean up incomplete tool calls in a thread."""
|
||||||
|
messages = await self.get_messages(thread_id)
|
||||||
Args:
|
|
||||||
thread_id: ID of the thread to clean up
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if cleanup was performed, False otherwise
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Adds failure results for incomplete tool calls
|
|
||||||
- Maintains thread consistency after interruptions
|
|
||||||
"""
|
|
||||||
messages = await self.list_messages(thread_id)
|
|
||||||
last_assistant_message = next((m for m in reversed(messages)
|
last_assistant_message = next((m for m in reversed(messages)
|
||||||
if m['role'] == 'assistant' and 'tool_calls' in m), None)
|
if m['role'] == 'assistant' and 'tool_calls' in m), None)
|
||||||
|
|
||||||
|
@ -253,10 +213,15 @@ class ThreadManager:
|
||||||
assistant_index = messages.index(last_assistant_message)
|
assistant_index = messages.index(last_assistant_message)
|
||||||
messages[assistant_index+1:assistant_index+1] = failed_tool_results
|
messages[assistant_index+1:assistant_index+1] = failed_tool_results
|
||||||
|
|
||||||
thread_path = os.path.join(self.threads_dir, f"{thread_id}.json")
|
async with self.db.transaction() as conn:
|
||||||
with open(thread_path, 'w') as f:
|
await conn.execute(
|
||||||
json.dump({"messages": messages}, f)
|
"""
|
||||||
|
UPDATE threads
|
||||||
|
SET messages = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE thread_id = ?
|
||||||
|
""",
|
||||||
|
(json.dumps(messages), thread_id)
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -326,7 +291,7 @@ class ThreadManager:
|
||||||
results_adder = XMLResultsAdder(self) if xml_tool_calling else StandardResultsAdder(self)
|
results_adder = XMLResultsAdder(self) if xml_tool_calling else StandardResultsAdder(self)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
messages = await self.list_messages(thread_id)
|
messages = await self.get_messages(thread_id)
|
||||||
prepared_messages = [system_message] + messages
|
prepared_messages = [system_message] + messages
|
||||||
if temporary_message:
|
if temporary_message:
|
||||||
prepared_messages.append(temporary_message)
|
prepared_messages.append(temporary_message)
|
||||||
|
@ -345,9 +310,8 @@ class ThreadManager:
|
||||||
available_functions=available_functions,
|
available_functions=available_functions,
|
||||||
add_message_callback=self.add_message,
|
add_message_callback=self.add_message,
|
||||||
update_message_callback=self._update_message,
|
update_message_callback=self._update_message,
|
||||||
list_messages_callback=self.list_messages,
|
get_messages_callback=self.get_messages,
|
||||||
parallel_tool_execution=parallel_tool_execution,
|
parallel_tool_execution=parallel_tool_execution,
|
||||||
threads_dir=self.threads_dir,
|
|
||||||
tool_parser=tool_parser,
|
tool_parser=tool_parser,
|
||||||
tool_executor=tool_executor,
|
tool_executor=tool_executor,
|
||||||
results_adder=results_adder
|
results_adder=results_adder
|
||||||
|
@ -405,25 +369,6 @@ class ThreadManager:
|
||||||
stream=stream
|
stream=stream
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _update_message(self, thread_id: str, message: Dict[str, Any]):
|
|
||||||
"""Update an existing message in the thread."""
|
|
||||||
thread_path = os.path.join(self.threads_dir, f"{thread_id}.json")
|
|
||||||
try:
|
|
||||||
with open(thread_path, 'r') as f:
|
|
||||||
thread_data = json.load(f)
|
|
||||||
|
|
||||||
# Find and update the last assistant message
|
|
||||||
for i in reversed(range(len(thread_data["messages"]))):
|
|
||||||
if thread_data["messages"][i]["role"] == "assistant":
|
|
||||||
thread_data["messages"][i] = message
|
|
||||||
break
|
|
||||||
|
|
||||||
with open(thread_path, 'w') as f:
|
|
||||||
json.dump(thread_data, f)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error updating message in thread {thread_id}: {e}")
|
|
||||||
raise e
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import asyncio
|
import asyncio
|
||||||
from agentpress.examples.example_agent.tools.files_tool import FilesTool
|
from agentpress.examples.example_agent.tools.files_tool import FilesTool
|
||||||
|
@ -503,7 +448,7 @@ if __name__ == "__main__":
|
||||||
print("\n✨ Response completed\n")
|
print("\n✨ Response completed\n")
|
||||||
|
|
||||||
# Display final thread state
|
# Display final thread state
|
||||||
messages = await thread_manager.list_messages(thread_id)
|
messages = await thread_manager.get_messages(thread_id)
|
||||||
print("\n📝 Final Thread State:")
|
print("\n📝 Final Thread State:")
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
role = msg.get('role', 'unknown')
|
role = msg.get('role', 'unknown')
|
||||||
|
|
|
@ -1,21 +1,8 @@
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from agentpress.thread_manager import ThreadManager
|
||||||
def load_thread_files(threads_dir: str):
|
from agentpress.db_connection import DBConnection
|
||||||
"""Load all thread files from the threads directory."""
|
import asyncio
|
||||||
thread_files = []
|
|
||||||
if os.path.exists(threads_dir):
|
|
||||||
for file in os.listdir(threads_dir):
|
|
||||||
if file.endswith('.json'):
|
|
||||||
thread_files.append(file)
|
|
||||||
return thread_files
|
|
||||||
|
|
||||||
def load_thread_content(thread_file: str, threads_dir: str):
|
|
||||||
"""Load the content of a specific thread file."""
|
|
||||||
with open(os.path.join(threads_dir, thread_file), 'r') as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
def format_message_content(content):
|
def format_message_content(content):
|
||||||
"""Format message content handling both string and list formats."""
|
"""Format message content handling both string and list formats."""
|
||||||
|
@ -31,56 +18,87 @@ def format_message_content(content):
|
||||||
return "\n".join(formatted_content)
|
return "\n".join(formatted_content)
|
||||||
return str(content)
|
return str(content)
|
||||||
|
|
||||||
|
async def load_threads():
|
||||||
|
"""Load all thread IDs from the database."""
|
||||||
|
db = DBConnection()
|
||||||
|
rows = await db.fetch_all("SELECT thread_id, created_at FROM threads ORDER BY created_at DESC")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
async def load_thread_content(thread_id: str):
|
||||||
|
"""Load the content of a specific thread from the database."""
|
||||||
|
thread_manager = ThreadManager()
|
||||||
|
return await thread_manager.get_messages(thread_id)
|
||||||
|
|
||||||
|
def render_message(role, content, avatar):
|
||||||
|
"""Render a message with a consistent chat-like style."""
|
||||||
|
# Create columns for avatar and message
|
||||||
|
col1, col2 = st.columns([1, 11])
|
||||||
|
|
||||||
|
# Style based on role
|
||||||
|
if role == "assistant":
|
||||||
|
bgcolor = "rgba(25, 25, 25, 0.05)"
|
||||||
|
elif role == "user":
|
||||||
|
bgcolor = "rgba(25, 120, 180, 0.05)"
|
||||||
|
elif role == "system":
|
||||||
|
bgcolor = "rgba(180, 25, 25, 0.05)"
|
||||||
|
else:
|
||||||
|
bgcolor = "rgba(100, 100, 100, 0.05)"
|
||||||
|
|
||||||
|
# Display avatar in first column
|
||||||
|
with col1:
|
||||||
|
st.markdown(f"<div style='text-align: center; font-size: 24px;'>{avatar}</div>", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# Display message in second column
|
||||||
|
with col2:
|
||||||
|
st.markdown(
|
||||||
|
f"""
|
||||||
|
<div style='background-color: {bgcolor}; padding: 10px; border-radius: 5px;'>
|
||||||
|
<strong>{role.upper()}</strong><br>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
unsafe_allow_html=True
|
||||||
|
)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
st.title("Thread Viewer")
|
st.title("Thread Viewer")
|
||||||
|
|
||||||
# Directory selection in sidebar
|
# Initialize thread data in session state
|
||||||
st.sidebar.title("Configuration")
|
if 'threads' not in st.session_state:
|
||||||
|
st.session_state.threads = asyncio.run(load_threads())
|
||||||
|
|
||||||
# Initialize session state with default directory
|
# Thread selection in sidebar
|
||||||
if 'threads_dir' not in st.session_state:
|
|
||||||
default_dir = "./threads"
|
|
||||||
if os.path.exists(default_dir):
|
|
||||||
st.session_state.threads_dir = default_dir
|
|
||||||
|
|
||||||
# Use Streamlit's file uploader for directory selection
|
|
||||||
uploaded_dir = st.sidebar.text_input(
|
|
||||||
"Enter threads directory path",
|
|
||||||
value="./threads" if not st.session_state.threads_dir else st.session_state.threads_dir,
|
|
||||||
placeholder="/path/to/threads",
|
|
||||||
help="Enter the full path to your threads directory"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Automatically load directory if it exists
|
|
||||||
if os.path.exists(uploaded_dir):
|
|
||||||
st.session_state.threads_dir = uploaded_dir
|
|
||||||
else:
|
|
||||||
st.sidebar.error("Directory not found!")
|
|
||||||
|
|
||||||
if st.session_state.threads_dir:
|
|
||||||
st.sidebar.success(f"Selected directory: {st.session_state.threads_dir}")
|
|
||||||
threads_dir = st.session_state.threads_dir
|
|
||||||
|
|
||||||
# Thread selection
|
|
||||||
st.sidebar.title("Select Thread")
|
st.sidebar.title("Select Thread")
|
||||||
thread_files = load_thread_files(threads_dir)
|
|
||||||
|
|
||||||
if not thread_files:
|
if not st.session_state.threads:
|
||||||
st.warning(f"No thread files found in '{threads_dir}'")
|
st.warning("No threads found in database")
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_thread = st.sidebar.selectbox(
|
# Format thread options with creation date
|
||||||
"Choose a thread file",
|
thread_options = {
|
||||||
thread_files,
|
f"{row[0]} ({datetime.fromisoformat(row[1]).strftime('%Y-%m-%d %H:%M')})"
|
||||||
format_func=lambda x: f"Thread: {x.replace('.json', '')}"
|
: row[0] for row in st.session_state.threads
|
||||||
|
}
|
||||||
|
|
||||||
|
selected_thread_display = st.sidebar.selectbox(
|
||||||
|
"Choose a thread",
|
||||||
|
options=list(thread_options.keys()),
|
||||||
)
|
)
|
||||||
|
|
||||||
if selected_thread:
|
if selected_thread_display:
|
||||||
thread_data = load_thread_content(selected_thread, threads_dir)
|
# Get the actual thread ID from the display string
|
||||||
messages = thread_data.get("messages", [])
|
selected_thread_id = thread_options[selected_thread_display]
|
||||||
|
|
||||||
# Display thread ID in sidebar
|
# Display thread ID in sidebar
|
||||||
st.sidebar.text(f"Thread ID: {selected_thread.replace('.json', '')}")
|
st.sidebar.text(f"Thread ID: {selected_thread_id}")
|
||||||
|
|
||||||
|
# Add refresh button
|
||||||
|
if st.sidebar.button("🔄 Refresh Thread"):
|
||||||
|
st.session_state.threads = asyncio.run(load_threads())
|
||||||
|
st.experimental_rerun()
|
||||||
|
|
||||||
|
# Load and display messages
|
||||||
|
messages = asyncio.run(load_thread_content(selected_thread_id))
|
||||||
|
|
||||||
# Display messages in chat-like interface
|
# Display messages in chat-like interface
|
||||||
for message in messages:
|
for message in messages:
|
||||||
|
@ -99,21 +117,24 @@ def main():
|
||||||
else:
|
else:
|
||||||
avatar = "❓"
|
avatar = "❓"
|
||||||
|
|
||||||
# Format the message container
|
# Format the content
|
||||||
with st.chat_message(role, avatar=avatar):
|
|
||||||
formatted_content = format_message_content(content)
|
formatted_content = format_message_content(content)
|
||||||
st.markdown(formatted_content)
|
|
||||||
|
|
||||||
|
# Render the message
|
||||||
|
render_message(role, formatted_content, avatar)
|
||||||
|
|
||||||
|
# Display tool calls if present
|
||||||
if "tool_calls" in message:
|
if "tool_calls" in message:
|
||||||
st.markdown("**Tool Calls:**")
|
with st.expander("🛠️ Tool Calls"):
|
||||||
for tool_call in message["tool_calls"]:
|
for tool_call in message["tool_calls"]:
|
||||||
st.code(
|
st.code(
|
||||||
f"Function: {tool_call['function']['name']}\n"
|
f"Function: {tool_call['function']['name']}\n"
|
||||||
f"Arguments: {tool_call['function']['arguments']}",
|
f"Arguments: {tool_call['function']['arguments']}",
|
||||||
language="json"
|
language="json"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
st.sidebar.warning("Please enter and load a threads directory")
|
# Add some spacing between messages
|
||||||
|
st.markdown("<br>", unsafe_allow_html=True)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -160,27 +160,44 @@ files = [
|
||||||
frozenlist = ">=1.1.0"
|
frozenlist = ">=1.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "altair"
|
name = "aiosqlite"
|
||||||
version = "5.4.1"
|
version = "0.20.0"
|
||||||
description = "Vega-Altair: A declarative statistical visualization library for Python."
|
description = "asyncio bridge to the standard sqlite3 module"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "altair-5.4.1-py3-none-any.whl", hash = "sha256:0fb130b8297a569d08991fb6fe763582e7569f8a04643bbd9212436e3be04aef"},
|
{file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"},
|
||||||
{file = "altair-5.4.1.tar.gz", hash = "sha256:0ce8c2e66546cb327e5f2d7572ec0e7c6feece816203215613962f0ec1d76a82"},
|
{file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
jinja2 = "*"
|
typing_extensions = ">=4.0"
|
||||||
jsonschema = ">=3.0"
|
|
||||||
narwhals = ">=1.5.2"
|
|
||||||
packaging = "*"
|
|
||||||
typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""}
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
all = ["altair-tiles (>=0.3.0)", "anywidget (>=0.9.0)", "numpy", "pandas (>=0.25.3)", "pyarrow (>=11)", "vega-datasets (>=0.9.0)", "vegafusion[embed] (>=1.6.6)", "vl-convert-python (>=1.6.0)"]
|
dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"]
|
||||||
dev = ["geopandas", "hatch", "ibis-framework[polars]", "ipython[kernel]", "mistune", "mypy", "pandas (>=0.25.3)", "pandas-stubs", "polars (>=0.20.3)", "pytest", "pytest-cov", "pytest-xdist[psutil] (>=3.5,<4.0)", "ruff (>=0.6.0)", "types-jsonschema", "types-setuptools"]
|
docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"]
|
||||||
doc = ["docutils", "jinja2", "myst-parser", "numpydoc", "pillow (>=9,<10)", "pydata-sphinx-theme (>=0.14.1)", "scipy", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinxext-altair"]
|
|
||||||
|
[[package]]
|
||||||
|
name = "altair"
|
||||||
|
version = "4.2.2"
|
||||||
|
description = "Altair: A declarative statistical visualization library for Python."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "altair-4.2.2-py3-none-any.whl", hash = "sha256:8b45ebeaf8557f2d760c5c77b79f02ae12aee7c46c27c06014febab6f849bc87"},
|
||||||
|
{file = "altair-4.2.2.tar.gz", hash = "sha256:39399a267c49b30d102c10411e67ab26374156a84b1aeb9fcd15140429ba49c5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
entrypoints = "*"
|
||||||
|
jinja2 = "*"
|
||||||
|
jsonschema = ">=3.0"
|
||||||
|
numpy = "*"
|
||||||
|
pandas = ">=0.18"
|
||||||
|
toolz = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["black", "docutils", "flake8", "ipython", "m2r", "mistune (<2.0.0)", "pytest", "recommonmark", "sphinx", "vega-datasets"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
|
@ -226,6 +243,19 @@ files = [
|
||||||
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
|
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asyncio"
|
||||||
|
version = "3.4.3"
|
||||||
|
description = "reference implementation of PEP 3156"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "asyncio-3.4.3-cp33-none-win32.whl", hash = "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de"},
|
||||||
|
{file = "asyncio-3.4.3-cp33-none-win_amd64.whl", hash = "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c"},
|
||||||
|
{file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"},
|
||||||
|
{file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "24.2.0"
|
version = "24.2.0"
|
||||||
|
@ -428,6 +458,17 @@ files = [
|
||||||
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
|
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "entrypoints"
|
||||||
|
version = "0.4"
|
||||||
|
description = "Discover and load entry points from installed packages."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"},
|
||||||
|
{file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exceptiongroup"
|
name = "exceptiongroup"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
@ -1140,25 +1181,6 @@ files = [
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
|
typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "narwhals"
|
|
||||||
version = "1.12.1"
|
|
||||||
description = "Extremely lightweight compatibility layer between dataframe libraries"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "narwhals-1.12.1-py3-none-any.whl", hash = "sha256:e251cb5fe4cabdcabb847d359f5de2b81df773df47e46f858fd5570c936919c4"},
|
|
||||||
{file = "narwhals-1.12.1.tar.gz", hash = "sha256:65ff0d1e8b509df8b52b395e8d5fe96751a68657bdabf0f3057a970ec2cd1809"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
cudf = ["cudf (>=23.08.00)"]
|
|
||||||
dask = ["dask[dataframe] (>=2024.7)"]
|
|
||||||
modin = ["modin"]
|
|
||||||
pandas = ["pandas (>=0.25.3)"]
|
|
||||||
polars = ["polars (>=0.20.3)"]
|
|
||||||
pyarrow = ["pyarrow (>=11.0.0)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
|
@ -1301,9 +1323,9 @@ files = [
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
numpy = [
|
numpy = [
|
||||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
|
||||||
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
||||||
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
||||||
|
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||||
]
|
]
|
||||||
python-dateutil = ">=2.8.2"
|
python-dateutil = ">=2.8.2"
|
||||||
pytz = ">=2020.1"
|
pytz = ">=2020.1"
|
||||||
|
@ -1690,8 +1712,8 @@ files = [
|
||||||
annotated-types = ">=0.6.0"
|
annotated-types = ">=0.6.0"
|
||||||
pydantic-core = "2.23.4"
|
pydantic-core = "2.23.4"
|
||||||
typing-extensions = [
|
typing-extensions = [
|
||||||
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
|
|
||||||
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
|
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
|
||||||
|
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
@ -2612,6 +2634,17 @@ files = [
|
||||||
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
|
{file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toolz"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "List processing tools and functional utilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"},
|
||||||
|
{file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tornado"
|
name = "tornado"
|
||||||
version = "6.4.1"
|
version = "6.4.1"
|
||||||
|
@ -2893,4 +2926,4 @@ type = ["pytest-mypy"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "d4c229cc0ecd64741dcf73186dce1c90100230e57acca3aa589e66dbb3052e9d"
|
content-hash = "32b3aefcb3a32a251cd1de7abaa721729c4e2ce2640625a80fcf51a0e3d5da2f"
|
||||||
|
|
|
@ -30,6 +30,9 @@ packaging = "^23.2"
|
||||||
setuptools = "^75.3.0"
|
setuptools = "^75.3.0"
|
||||||
pytest = "^8.3.3"
|
pytest = "^8.3.3"
|
||||||
pytest-asyncio = "^0.24.0"
|
pytest-asyncio = "^0.24.0"
|
||||||
|
aiosqlite = "^0.20.0"
|
||||||
|
asyncio = "^3.4.3"
|
||||||
|
altair = "4.2.2"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
agentpress = "agentpress.cli:main"
|
agentpress = "agentpress.cli:main"
|
||||||
|
|
Loading…
Reference in New Issue