From cb9f7b616fdb7dc4d30cec6efa557abe2993c9ee Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Tue, 19 Nov 2024 03:46:25 +0100 Subject: [PATCH] - 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 --- CHANGELOG.md | 8 + agentpress.db | Bin 0 -> 110592 bytes agentpress/agents/simple_web_dev/agent.py | 12 +- .../agents/simple_web_dev/tools/files_tool.py | 7 +- .../simple_web_dev/tools/terminal_tool.py | 5 +- agentpress/cli.py | 16 +- agentpress/db_connection.py | 125 +++++++ agentpress/{ => processor}/base_processors.py | 4 +- .../{ => processor}/llm_response_processor.py | 21 +- .../standard}/standard_results_adder.py | 4 +- .../standard}/standard_tool_executor.py | 2 +- .../standard}/standard_tool_parser.py | 2 +- .../{ => processor/xml}/xml_results_adder.py | 4 +- .../{ => processor/xml}/xml_tool_executor.py | 2 +- .../{ => processor/xml}/xml_tool_parser.py | 2 +- agentpress/state_manager.py | 183 +++++---- agentpress/thread_manager.py | 349 ++++++++---------- agentpress/thread_viewer_ui.py | 195 +++++----- poetry.lock | 103 ++++-- pyproject.toml | 3 + 20 files changed, 616 insertions(+), 431 deletions(-) create mode 100644 agentpress.db create mode 100644 agentpress/db_connection.py rename agentpress/{ => processor}/base_processors.py (98%) rename agentpress/{ => processor}/llm_response_processor.py (93%) rename agentpress/{ => processor/standard}/standard_results_adder.py (96%) rename agentpress/{ => processor/standard}/standard_tool_executor.py (99%) rename agentpress/{ => processor/standard}/standard_tool_parser.py (98%) rename agentpress/{ => processor/xml}/xml_results_adder.py (96%) rename agentpress/{ => processor/xml}/xml_tool_executor.py (98%) rename agentpress/{ => processor/xml}/xml_tool_parser.py (99%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f229936e..029717a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - Added base processor classes for extensible tool handling: - ToolParserBase: Abstract base class for parsing LLM responses diff --git a/agentpress.db b/agentpress.db new file mode 100644 index 0000000000000000000000000000000000000000..42d82dccb262ea256ba721dbb2d91e387c93331f GIT binary patch literal 110592 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 xn14W_qQ6wQ^2ai?{t<pr literal 0 HcmV?d00001 diff --git a/agentpress/agents/simple_web_dev/agent.py b/agentpress/agents/simple_web_dev/agent.py index 54dd3486..5300ed74 100644 --- a/agentpress/agents/simple_web_dev/agent.py +++ b/agentpress/agents/simple_web_dev/agent.py @@ -91,12 +91,13 @@ file contents here async def run_agent(thread_id: str, use_xml: bool = True, max_iterations: int = 5): """Run the development agent with specified configuration.""" thread_manager = ThreadManager() - state_manager = StateManager() - - thread_manager.add_tool(FilesTool) - thread_manager.add_tool(TerminalTool) - # Combine base message with XML format if needed + store_id = await StateManager.create_store() + state_manager = StateManager(store_id) + + thread_manager.add_tool(FilesTool, store_id=store_id) + thread_manager.add_tool(TerminalTool, store_id=store_id) + system_message = { "role": "system", "content": BASE_SYSTEM_MESSAGE + (XML_FORMAT if use_xml else "") @@ -199,6 +200,7 @@ def main(): async def async_main(): thread_manager = ThreadManager() + thread_id = await thread_manager.create_thread() await thread_manager.add_message( thread_id, diff --git a/agentpress/agents/simple_web_dev/tools/files_tool.py b/agentpress/agents/simple_web_dev/tools/files_tool.py index c27159b6..d87ae9b9 100644 --- a/agentpress/agents/simple_web_dev/tools/files_tool.py +++ b/agentpress/agents/simple_web_dev/tools/files_tool.py @@ -1,8 +1,9 @@ import os import asyncio 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 typing import Optional class FilesTool(Tool): """File management tool for creating, updating, and deleting files. @@ -53,11 +54,11 @@ class FilesTool(Tool): ".sql" } - def __init__(self): + def __init__(self, store_id: Optional[str] = None): super().__init__() self.workspace = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'workspace') 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 asyncio.create_task(self._init_workspace_state()) diff --git a/agentpress/agents/simple_web_dev/tools/terminal_tool.py b/agentpress/agents/simple_web_dev/tools/terminal_tool.py index 5bd7eb77..616093fc 100644 --- a/agentpress/agents/simple_web_dev/tools/terminal_tool.py +++ b/agentpress/agents/simple_web_dev/tools/terminal_tool.py @@ -3,15 +3,16 @@ import asyncio import subprocess from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema from agentpress.state_manager import StateManager +from typing import Optional class TerminalTool(Tool): """Terminal command execution tool for workspace operations.""" - def __init__(self): + def __init__(self, store_id: Optional[str] = None): super().__init__() self.workspace = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'workspace') 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): """Update command history in state""" diff --git a/agentpress/cli.py b/agentpress/cli.py index 5cb94cb5..24ee9911 100644 --- a/agentpress/cli.py +++ b/agentpress/cli.py @@ -26,14 +26,14 @@ MODULES = { "processors": { "required": True, "files": [ - "base_processors.py", - "llm_response_processor.py", - "standard_tool_parser.py", - "standard_tool_executor.py", - "standard_results_adder.py", - "xml_tool_parser.py", - "xml_tool_executor.py", - "xml_results_adder.py" + "processor/base_processors.py", + "processor/llm_response_processor.py", + "processor/standard/standard_tool_parser.py", + "processor/standard/standard_tool_executor.py", + "processor/standard/standard_results_adder.py", + "processor/xml/xml_tool_parser.py", + "processor/xml/xml_tool_executor.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." }, diff --git a/agentpress/db_connection.py b/agentpress/db_connection.py new file mode 100644 index 00000000..c173c815 --- /dev/null +++ b/agentpress/db_connection.py @@ -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() \ No newline at end of file diff --git a/agentpress/base_processors.py b/agentpress/processor/base_processors.py similarity index 98% rename from agentpress/base_processors.py rename to agentpress/processor/base_processors.py index 85f6eea4..ec724180 100644 --- a/agentpress/base_processors.py +++ b/agentpress/processor/base_processors.py @@ -172,7 +172,7 @@ class ResultsAdderBase(ABC): Attributes: add_message: Callback for adding new 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 """ @@ -184,7 +184,7 @@ class ResultsAdderBase(ABC): """ self.add_message = thread_manager.add_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 @abstractmethod diff --git a/agentpress/llm_response_processor.py b/agentpress/processor/llm_response_processor.py similarity index 93% rename from agentpress/llm_response_processor.py rename to agentpress/processor/llm_response_processor.py index 5bf6bc7a..24572210 100644 --- a/agentpress/llm_response_processor.py +++ b/agentpress/processor/llm_response_processor.py @@ -11,10 +11,10 @@ This module provides comprehensive processing of LLM responses, including: import asyncio from typing import Callable, Dict, Any, AsyncGenerator, Optional import logging -from agentpress.base_processors import ToolParserBase, ToolExecutorBase, ResultsAdderBase -from agentpress.standard_tool_parser import StandardToolParser -from agentpress.standard_tool_executor import StandardToolExecutor -from agentpress.standard_results_adder import StandardResultsAdder +from agentpress.processor.base_processors import ToolParserBase, ToolExecutorBase, ResultsAdderBase +from agentpress.processor.standard.standard_tool_parser import StandardToolParser +from agentpress.processor.standard.standard_tool_executor import StandardToolExecutor +from agentpress.processor.standard.standard_results_adder import StandardResultsAdder class LLMResponseProcessor: """Handles LLM response processing and tool execution management. @@ -40,9 +40,8 @@ class LLMResponseProcessor: available_functions: Dict = None, add_message_callback: Callable = None, update_message_callback: Callable = None, - list_messages_callback: Callable = None, + get_messages_callback: Callable = None, parallel_tool_execution: bool = True, - threads_dir: str = "threads", tool_parser: Optional[ToolParserBase] = None, tool_executor: Optional[ToolExecutorBase] = None, results_adder: Optional[ResultsAdderBase] = None, @@ -55,9 +54,8 @@ class LLMResponseProcessor: available_functions: Dictionary of available tool functions add_message_callback: Callback for adding 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 - threads_dir: Directory for thread storage tool_parser: Custom tool parser implementation tool_executor: Custom tool executor 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_parser = tool_parser or StandardToolParser() self.available_functions = available_functions or {} - self.threads_dir = threads_dir # 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: def __init__(self, add_msg, update_msg, list_msg): self.add_message = add_msg self._update_message = update_msg - self.list_messages = list_msg - thread_manager = MinimalThreadManager(add_message_callback, update_message_callback, list_messages_callback) + self.get_messages = list_msg + thread_manager = MinimalThreadManager(add_message_callback, update_message_callback, get_messages_callback) self.results_adder = results_adder or StandardResultsAdder(thread_manager) diff --git a/agentpress/standard_results_adder.py b/agentpress/processor/standard/standard_results_adder.py similarity index 96% rename from agentpress/standard_results_adder.py rename to agentpress/processor/standard/standard_results_adder.py index 8862bab8..49d08b64 100644 --- a/agentpress/standard_results_adder.py +++ b/agentpress/processor/standard/standard_results_adder.py @@ -1,5 +1,5 @@ from typing import Dict, Any, List, Optional -from agentpress.base_processors import ResultsAdderBase +from agentpress.processor.base_processors import ResultsAdderBase # --- Standard Results Adder Implementation --- @@ -81,6 +81,6 @@ class StandardResultsAdder(ResultsAdderBase): - Checks for duplicate tool results before adding - 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): await self.add_message(thread_id, result) diff --git a/agentpress/standard_tool_executor.py b/agentpress/processor/standard/standard_tool_executor.py similarity index 99% rename from agentpress/standard_tool_executor.py rename to agentpress/processor/standard/standard_tool_executor.py index e70cd966..575b9d72 100644 --- a/agentpress/standard_tool_executor.py +++ b/agentpress/processor/standard/standard_tool_executor.py @@ -9,7 +9,7 @@ import asyncio import json import logging 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 # --- Standard Tool Executor Implementation --- diff --git a/agentpress/standard_tool_parser.py b/agentpress/processor/standard/standard_tool_parser.py similarity index 98% rename from agentpress/standard_tool_parser.py rename to agentpress/processor/standard/standard_tool_parser.py index 9ca551b9..ae024462 100644 --- a/agentpress/standard_tool_parser.py +++ b/agentpress/processor/standard/standard_tool_parser.py @@ -1,6 +1,6 @@ import json from typing import Dict, Any, Optional -from agentpress.base_processors import ToolParserBase +from agentpress.processor.base_processors import ToolParserBase # --- Standard Tool Parser Implementation --- diff --git a/agentpress/xml_results_adder.py b/agentpress/processor/xml/xml_results_adder.py similarity index 96% rename from agentpress/xml_results_adder.py rename to agentpress/processor/xml/xml_results_adder.py index 97822266..49593da8 100644 --- a/agentpress/xml_results_adder.py +++ b/agentpress/processor/xml/xml_results_adder.py @@ -1,6 +1,6 @@ import logging from typing import Dict, Any, List, Optional -from agentpress.base_processors import ResultsAdderBase +from agentpress.processor.base_processors import ResultsAdderBase class XMLResultsAdder(ResultsAdderBase): """XML-specific implementation for handling tool results and message processing. @@ -79,7 +79,7 @@ class XMLResultsAdder(ResultsAdderBase): """ try: # 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) if msg['role'] == 'assistant'), None) diff --git a/agentpress/xml_tool_executor.py b/agentpress/processor/xml/xml_tool_executor.py similarity index 98% rename from agentpress/xml_tool_executor.py rename to agentpress/processor/xml/xml_tool_executor.py index 1cf701bd..185e8e6c 100644 --- a/agentpress/xml_tool_executor.py +++ b/agentpress/processor/xml/xml_tool_executor.py @@ -9,7 +9,7 @@ from typing import List, Dict, Any, Set, Callable, Optional import asyncio import json import logging -from agentpress.base_processors import ToolExecutorBase +from agentpress.processor.base_processors import ToolExecutorBase from agentpress.tool import ToolResult from agentpress.tool_registry import ToolRegistry diff --git a/agentpress/xml_tool_parser.py b/agentpress/processor/xml/xml_tool_parser.py similarity index 99% rename from agentpress/xml_tool_parser.py rename to agentpress/processor/xml/xml_tool_parser.py index 8942a4ae..890c00aa 100644 --- a/agentpress/xml_tool_parser.py +++ b/agentpress/processor/xml/xml_tool_parser.py @@ -7,7 +7,7 @@ complete and streaming responses with robust XML parsing and validation capabili import logging from typing import Dict, Any, Optional, List, Tuple -from agentpress.base_processors import ToolParserBase +from agentpress.processor.base_processors import ToolParserBase import json import re from agentpress.tool_registry import ToolRegistry diff --git a/agentpress/state_manager.py b/agentpress/state_manager.py index c822e862..a3b88ad7 100644 --- a/agentpress/state_manager.py +++ b/agentpress/state_manager.py @@ -1,76 +1,100 @@ import json -import os import logging -from typing import Any +from typing import Any, Optional, List, Dict, Union, AsyncGenerator from asyncio import Lock from contextlib import asynccontextmanager +import uuid +from agentpress.db_connection import DBConnection +import asyncio class StateManager: """ Manages persistent state storage for AgentPress components. - The StateManager provides thread-safe access to a JSON-based state store, - allowing components to save and retrieve data across sessions. It handles - concurrent access using asyncio locks and provides atomic operations for - state modifications. + The StateManager provides thread-safe access to a SQLite-based state store, + allowing components to save and retrieve data across sessions. Each store + has a unique ID and contains multiple key-value pairs in a single JSON object. Attributes: 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: - store_file (str): Path to the JSON file to store state. - Defaults to "state.json" in the current directory. + store_id (str, optional): Unique identifier for the store. If None, creates new. """ self.lock = Lock() - self.store_file = store_file - logging.info(f"StateManager initialized with store file: {store_file}") + self.db = DBConnection() + 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 async def store_scope(self): """ Context manager for atomic state operations. - Provides thread-safe access to the state store, handling file I/O - and ensuring proper cleanup. Automatically loads the current state - and saves changes when the context exits. + Provides thread-safe access to the state store, handling database + operations and ensuring proper cleanup. Yields: dict: The current state store contents Raises: - Exception: If there are errors reading from or writing to the store file + Exception: If there are errors with database operations """ - try: - # Read current state - if os.path.exists(self.store_file): - with open(self.store_file, 'r') as f: - store = json.load(f) - else: - store = {} - - yield store - - # Write updated state - with open(self.store_file, 'w') as f: - json.dump(store, f, indent=2) - logging.debug("Store saved successfully") - except Exception as e: - logging.error("Error in store operation", exc_info=True) - raise + async with self.lock: + try: + async with self.db.transaction() as conn: + async with conn.execute( + "SELECT store_data FROM state_stores WHERE store_id = ?", + (self.store_id,) + ) as cursor: + row = await cursor.fetchone() + store = json.loads(row[0]) if row else {} + + yield store + + await conn.execute( + """ + UPDATE state_stores + SET store_data = ?, updated_at = CURRENT_TIMESTAMP + WHERE store_id = ? + """, + (json.dumps(store), self.store_id) + ) + except Exception as e: + logging.error("Error in store operation", exc_info=True) + 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: 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: Any: The stored data @@ -78,17 +102,12 @@ class StateManager: Raises: Exception: If there are errors during storage operation """ - async with self.lock: - async with self.store_scope() as store: - try: - store[key] = data # Will be JSON serialized when written to file - logging.info(f'Updated store key: {key}') - return data - except Exception as e: - logging.error(f'Error in set: {str(e)}') - raise + async with self.store_scope() as store: + store[key] = data + logging.info(f'Updated store key: {key}') + return data - async def get(self, key: str) -> Any: + async def get(self, key: str) -> Optional[Any]: """ Get data for a key. @@ -97,9 +116,6 @@ class StateManager: Returns: 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: if key in store: @@ -115,17 +131,31 @@ class StateManager: Args: 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: - if key in store: - del store[key] - logging.info(f"Deleted key: {key}") - else: - logging.info(f"Key not found for deletion: {key}") + async with self.store_scope() as store: + if key in store: + del store[key] + logging.info(f"Deleted key: {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: """ @@ -133,9 +163,6 @@ class StateManager: Returns: 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: logging.info(f"Store content: {store}") @@ -148,7 +175,29 @@ class StateManager: Removes all data from the store, resetting it to an empty state. This operation is atomic and thread-safe. """ - async with self.lock: - async with self.store_scope() as store: - store.clear() - logging.info("Cleared store") + async with self.store_scope() as store: + store.clear() + 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 diff --git a/agentpress/thread_manager.py b/agentpress/thread_manager.py index 7a5c7802..61eac089 100644 --- a/agentpress/thread_manager.py +++ b/agentpress/thread_manager.py @@ -11,21 +11,22 @@ This module provides comprehensive conversation management, including: import json import logging -import os +import asyncio import uuid from typing import List, Dict, Any, Optional, Type, Union, AsyncGenerator from agentpress.llm import make_llm_api_call from agentpress.tool import Tool, ToolResult from agentpress.tool_registry import ToolRegistry -from agentpress.llm_response_processor import LLMResponseProcessor -from agentpress.base_processors import ToolParserBase, ToolExecutorBase, ResultsAdderBase +from agentpress.processor.llm_response_processor import LLMResponseProcessor +from agentpress.processor.base_processors import ToolParserBase, ToolExecutorBase, ResultsAdderBase +from agentpress.db_connection import DBConnection -from agentpress.xml_tool_parser import XMLToolParser -from agentpress.xml_tool_executor import XMLToolExecutor -from agentpress.xml_results_adder import XMLResultsAdder -from agentpress.standard_tool_parser import StandardToolParser -from agentpress.standard_tool_executor import StandardToolExecutor -from agentpress.standard_results_adder import StandardResultsAdder +from agentpress.processor.xml.xml_tool_parser import XMLToolParser +from agentpress.processor.xml.xml_tool_executor import XMLToolExecutor +from agentpress.processor.xml.xml_results_adder import XMLResultsAdder +from agentpress.processor.standard.standard_tool_parser import StandardToolParser +from agentpress.processor.standard.standard_tool_executor import StandardToolExecutor +from agentpress.processor.standard.standard_results_adder import StandardResultsAdder class ThreadManager: """Manages conversation threads with LLM models and tool execution. @@ -33,204 +34,163 @@ class ThreadManager: Provides comprehensive conversation management, handling message threading, tool registration, and LLM interactions with support for both standard and 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"): - """Initialize ThreadManager. - - Args: - threads_dir: Directory to store thread files - - Notes: - Creates the threads directory if it doesn't exist - """ - self.threads_dir = threads_dir + def __init__(self): + """Initialize ThreadManager.""" + self.db = DBConnection() 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): - """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 - """ + """Add a tool to the ThreadManager.""" self.tool_registry.register_tool(tool_class, function_names, **kwargs) async def create_thread(self) -> str: - """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 - """ + """Create a new conversation thread.""" thread_id = str(uuid.uuid4()) - thread_path = os.path.join(self.threads_dir, f"{thread_id}.json") - with open(thread_path, 'w') as f: - json.dump({"messages": []}, f) + await self.db.execute( + "INSERT INTO threads (thread_id, messages) VALUES (?, ?)", + (thread_id, json.dumps([])) + ) return thread_id 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. - - 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 - """ + """Add a message to an existing thread.""" 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: - with open(thread_path, 'r') as f: - thread_data = json.load(f) - - messages = thread_data["messages"] - - # Handle cleanup of incomplete tool calls - if message_data['role'] == 'user': - 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 last_assistant_index is not None: - tool_call_count = len(messages[last_assistant_index]['tool_calls']) - tool_response_count = sum(1 for msg in messages[last_assistant_index+1:] - if msg['role'] == 'tool') + async with self.db.transaction() as conn: + # Handle cleanup of incomplete tool calls + if message_data['role'] == 'user': + messages = await self.get_messages(thread_id) + 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 tool_call_count != tool_response_count: - await self.cleanup_incomplete_tool_calls(thread_id) + if last_assistant_index is not None: + tool_call_count = len(messages[last_assistant_index]['tool_calls']) + tool_response_count = sum(1 for msg in messages[last_assistant_index+1:] + if msg['role'] == 'tool') + + if tool_call_count != tool_response_count: + await self.cleanup_incomplete_tool_calls(thread_id) - # Convert ToolResult instances to strings - for key, value in message_data.items(): - if isinstance(value, ToolResult): - message_data[key] = str(value) + # Convert ToolResult instances to strings + for key, value in message_data.items(): + if isinstance(value, ToolResult): + message_data[key] = str(value) - # Handle image attachments - if images: - if isinstance(message_data['content'], str): - message_data['content'] = [{"type": "text", "text": message_data['content']}] - elif not isinstance(message_data['content'], list): - message_data['content'] = [] + # Handle image attachments + if images: + if isinstance(message_data['content'], str): + message_data['content'] = [{"type": "text", "text": message_data['content']}] + elif not isinstance(message_data['content'], list): + message_data['content'] = [] - for image in images: - image_content = { - "type": "image_url", - "image_url": { - "url": f"data:{image['content_type']};base64,{image['base64']}", - "detail": "high" + for image in images: + image_content = { + "type": "image_url", + "image_url": { + "url": f"data:{image['content_type']};base64,{image['base64']}", + "detail": "high" + } } - } - message_data['content'].append(image_content) + message_data['content'].append(image_content) - messages.append(message_data) - thread_data["messages"] = messages + # Get current 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") + + messages = json.loads(row[0]) + 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}") - with open(thread_path, 'w') as f: - json.dump(thread_data, f) - - logging.info(f"Message added to thread {thread_id}: {message_data}") except Exception as e: logging.error(f"Failed to add message to thread {thread_id}: {e}") raise e - async def list_messages( + async def get_messages( self, - thread_id: str, - hide_tool_msgs: bool = False, - only_latest_assistant: bool = False, + thread_id: str, + hide_tool_msgs: bool = False, + only_latest_assistant: bool = False, regular_list: bool = True ) -> List[Dict[str, Any]]: - """Retrieve messages from a thread with optional filtering. - - Args: - 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: - for msg in reversed(messages): - if msg.get('role') == 'assistant': - return [msg] - return [] - - filtered_messages = messages - - if hide_tool_msgs: - filtered_messages = [ - {k: v for k, v in msg.items() if k != 'tool_calls'} - for msg in filtered_messages - if msg.get('role') != 'tool' - ] - - if regular_list: - filtered_messages = [ - msg for msg in filtered_messages - if msg.get('role') in ['system', 'assistant', 'tool', 'user'] - ] - - return filtered_messages - except FileNotFoundError: + """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 [] + + messages = json.loads(row[0]) + + if only_latest_assistant: + for msg in reversed(messages): + if msg.get('role') == 'assistant': + return [msg] + return [] + + if hide_tool_msgs: + messages = [ + {k: v for k, v in msg.items() if k != 'tool_calls'} + for msg in messages + if msg.get('role') != 'tool' + ] + + if regular_list: + messages = [ + msg for msg in messages + if msg.get('role') in ['system', 'assistant', 'tool', 'user'] + ] + + return messages + + 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): - """Clean up incomplete tool calls in a thread. - - 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) + """Clean up incomplete tool calls in a thread.""" + messages = await self.get_messages(thread_id) last_assistant_message = next((m for m in reversed(messages) if m['role'] == 'assistant' and 'tool_calls' in m), None) @@ -253,10 +213,15 @@ class ThreadManager: assistant_index = messages.index(last_assistant_message) messages[assistant_index+1:assistant_index+1] = failed_tool_results - thread_path = os.path.join(self.threads_dir, f"{thread_id}.json") - with open(thread_path, 'w') as f: - json.dump({"messages": messages}, f) - + async with self.db.transaction() as conn: + await conn.execute( + """ + UPDATE threads + SET messages = ?, updated_at = CURRENT_TIMESTAMP + WHERE thread_id = ? + """, + (json.dumps(messages), thread_id) + ) return True return False @@ -326,7 +291,7 @@ class ThreadManager: results_adder = XMLResultsAdder(self) if xml_tool_calling else StandardResultsAdder(self) try: - messages = await self.list_messages(thread_id) + messages = await self.get_messages(thread_id) prepared_messages = [system_message] + messages if temporary_message: prepared_messages.append(temporary_message) @@ -345,9 +310,8 @@ class ThreadManager: available_functions=available_functions, add_message_callback=self.add_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, - threads_dir=self.threads_dir, tool_parser=tool_parser, tool_executor=tool_executor, results_adder=results_adder @@ -405,25 +369,6 @@ class ThreadManager: 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__": import asyncio from agentpress.examples.example_agent.tools.files_tool import FilesTool @@ -503,7 +448,7 @@ if __name__ == "__main__": print("\n✨ Response completed\n") # 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:") for msg in messages: role = msg.get('role', 'unknown') diff --git a/agentpress/thread_viewer_ui.py b/agentpress/thread_viewer_ui.py index b14f12c3..944cb499 100644 --- a/agentpress/thread_viewer_ui.py +++ b/agentpress/thread_viewer_ui.py @@ -1,21 +1,8 @@ import streamlit as st -import json -import os from datetime import datetime - -def load_thread_files(threads_dir: str): - """Load all thread files from the threads directory.""" - 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) +from agentpress.thread_manager import ThreadManager +from agentpress.db_connection import DBConnection +import asyncio def format_message_content(content): """Format message content handling both string and list formats.""" @@ -31,89 +18,123 @@ def format_message_content(content): return "\n".join(formatted_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"
{avatar}
", unsafe_allow_html=True) + + # Display message in second column + with col2: + st.markdown( + f""" +
+ {role.upper()}
+ {content} +
+ """, + unsafe_allow_html=True + ) + def main(): st.title("Thread Viewer") - # Directory selection in sidebar - st.sidebar.title("Configuration") + # Initialize thread data in session state + if 'threads' not in st.session_state: + st.session_state.threads = asyncio.run(load_threads()) - # Initialize session state with default directory - 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" + # Thread selection in sidebar + st.sidebar.title("Select Thread") + + if not st.session_state.threads: + st.warning("No threads found in database") + return + + # Format thread options with creation date + thread_options = { + f"{row[0]} ({datetime.fromisoformat(row[1]).strftime('%Y-%m-%d %H:%M')})" + : row[0] for row in st.session_state.threads + } + + selected_thread_display = st.sidebar.selectbox( + "Choose a thread", + options=list(thread_options.keys()), ) - - # 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 + if selected_thread_display: + # Get the actual thread ID from the display string + selected_thread_id = thread_options[selected_thread_display] - # Thread selection - st.sidebar.title("Select Thread") - thread_files = load_thread_files(threads_dir) + # Display thread ID in sidebar + st.sidebar.text(f"Thread ID: {selected_thread_id}") - if not thread_files: - st.warning(f"No thread files found in '{threads_dir}'") - return + # Add refresh button + if st.sidebar.button("šŸ”„ Refresh Thread"): + st.session_state.threads = asyncio.run(load_threads()) + st.experimental_rerun() - selected_thread = st.sidebar.selectbox( - "Choose a thread file", - thread_files, - format_func=lambda x: f"Thread: {x.replace('.json', '')}" - ) + # Load and display messages + messages = asyncio.run(load_thread_content(selected_thread_id)) - if selected_thread: - thread_data = load_thread_content(selected_thread, threads_dir) - messages = thread_data.get("messages", []) + # Display messages in chat-like interface + for message in messages: + role = message.get("role", "unknown") + content = message.get("content", "") - # Display thread ID in sidebar - st.sidebar.text(f"Thread ID: {selected_thread.replace('.json', '')}") + # Determine avatar based on role + if role == "assistant": + avatar = "šŸ¤–" + elif role == "user": + avatar = "šŸ‘¤" + elif role == "system": + avatar = "āš™ļø" + elif role == "tool": + avatar = "šŸ”§" + else: + avatar = "ā“" - # Display messages in chat-like interface - for message in messages: - role = message.get("role", "unknown") - content = message.get("content", "") - - # Determine avatar based on role - if role == "assistant": - avatar = "šŸ¤–" - elif role == "user": - avatar = "šŸ‘¤" - elif role == "system": - avatar = "āš™ļø" - elif role == "tool": - avatar = "šŸ”§" - else: - avatar = "ā“" - - # Format the message container - with st.chat_message(role, avatar=avatar): - formatted_content = format_message_content(content) - st.markdown(formatted_content) - - if "tool_calls" in message: - st.markdown("**Tool Calls:**") - for tool_call in message["tool_calls"]: - st.code( - f"Function: {tool_call['function']['name']}\n" - f"Arguments: {tool_call['function']['arguments']}", - language="json" - ) - else: - st.sidebar.warning("Please enter and load a threads directory") + # Format the content + formatted_content = format_message_content(content) + + # Render the message + render_message(role, formatted_content, avatar) + + # Display tool calls if present + if "tool_calls" in message: + with st.expander("šŸ› ļø Tool Calls"): + for tool_call in message["tool_calls"]: + st.code( + f"Function: {tool_call['function']['name']}\n" + f"Arguments: {tool_call['function']['arguments']}", + language="json" + ) + + # Add some spacing between messages + st.markdown("
", unsafe_allow_html=True) if __name__ == "__main__": main() diff --git a/poetry.lock b/poetry.lock index 80963eec..680a3bb7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -160,27 +160,44 @@ files = [ frozenlist = ">=1.1.0" [[package]] -name = "altair" -version = "5.4.1" -description = "Vega-Altair: A declarative statistical visualization library for Python." +name = "aiosqlite" +version = "0.20.0" +description = "asyncio bridge to the standard sqlite3 module" optional = false python-versions = ">=3.8" files = [ - {file = "altair-5.4.1-py3-none-any.whl", hash = "sha256:0fb130b8297a569d08991fb6fe763582e7569f8a04643bbd9212436e3be04aef"}, - {file = "altair-5.4.1.tar.gz", hash = "sha256:0ce8c2e66546cb327e5f2d7572ec0e7c6feece816203215613962f0ec1d76a82"}, + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, ] [package.dependencies] -jinja2 = "*" -jsonschema = ">=3.0" -narwhals = ">=1.5.2" -packaging = "*" -typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} +typing_extensions = ">=4.0" [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 = ["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"] -doc = ["docutils", "jinja2", "myst-parser", "numpydoc", "pillow (>=9,<10)", "pydata-sphinx-theme (>=0.14.1)", "scipy", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinxext-altair"] +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)"] +docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] + +[[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]] name = "annotated-types" @@ -226,6 +243,19 @@ files = [ {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]] name = "attrs" version = "24.2.0" @@ -428,6 +458,17 @@ files = [ {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]] name = "exceptiongroup" version = "1.2.2" @@ -1140,25 +1181,6 @@ files = [ [package.dependencies] 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]] name = "numpy" version = "2.0.2" @@ -1301,9 +1323,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.22.4", 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" pytz = ">=2020.1" @@ -1690,8 +1712,8 @@ files = [ annotated-types = ">=0.6.0" pydantic-core = "2.23.4" typing-extensions = [ - {version = ">=4.12.2", 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] @@ -2612,6 +2634,17 @@ files = [ {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]] name = "tornado" version = "6.4.1" @@ -2893,4 +2926,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "d4c229cc0ecd64741dcf73186dce1c90100230e57acca3aa589e66dbb3052e9d" +content-hash = "32b3aefcb3a32a251cd1de7abaa721729c4e2ce2640625a80fcf51a0e3d5da2f" diff --git a/pyproject.toml b/pyproject.toml index 6e82fadc..411141f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,9 @@ packaging = "^23.2" setuptools = "^75.3.0" pytest = "^8.3.3" pytest-asyncio = "^0.24.0" +aiosqlite = "^0.20.0" +asyncio = "^3.4.3" +altair = "4.2.2" [tool.poetry.scripts] agentpress = "agentpress.cli:main"