mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-12 01:41:26 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f2c4d10b5 | ||
|
|
42a86c831f | ||
|
|
48443b11d6 | ||
|
|
71745aa14a | ||
|
|
4f309ec16d |
No files matched your search
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.8.4" \
|
||||
"praisonai>=2.8.9" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.8.4" \
|
||||
"praisonai>=2.8.9" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.8.4" \
|
||||
"praisonai>=2.8.9" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# ACP (Agent Client Protocol) Examples
|
||||
|
||||
This directory contains examples for using PraisonAI with the Agent Client Protocol (ACP).
|
||||
|
||||
## What is ACP?
|
||||
|
||||
The Agent Client Protocol (ACP) is a standardized JSON-RPC 2.0 protocol that allows code editors and IDEs to communicate with AI coding agents. It enables seamless integration between PraisonAI and editors like:
|
||||
|
||||
- **Zed**
|
||||
- **JetBrains IDEs** (IntelliJ, PyCharm, WebStorm, etc.)
|
||||
- **VSCode** (via extensions)
|
||||
- **Toad**
|
||||
|
||||
## Quick Start
|
||||
|
||||
### CLI Usage
|
||||
|
||||
```bash
|
||||
# Start ACP server with defaults
|
||||
praisonai acp
|
||||
|
||||
# Enable debug logging
|
||||
praisonai acp --debug
|
||||
|
||||
# Allow file writes
|
||||
praisonai acp --allow-write
|
||||
|
||||
# Use a specific model
|
||||
praisonai acp -m gpt-4o
|
||||
|
||||
# Resume last session
|
||||
praisonai acp --resume --last
|
||||
```
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
from praisonai.acp import serve
|
||||
|
||||
serve(
|
||||
workspace=".",
|
||||
debug=True,
|
||||
allow_write=True,
|
||||
)
|
||||
```
|
||||
|
||||
## Editor Configuration
|
||||
|
||||
### Zed
|
||||
|
||||
Add to `~/.config/zed/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_servers": {
|
||||
"PraisonAI": {
|
||||
"command": "praisonai",
|
||||
"args": ["acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JetBrains
|
||||
|
||||
Add to `~/.jetbrains/acp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_servers": {
|
||||
"PraisonAI": {
|
||||
"command": "praisonai",
|
||||
"args": ["acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Toad
|
||||
|
||||
```bash
|
||||
toad acp "praisonai acp"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- `basic_acp_server.py` - Simple ACP server with default settings
|
||||
- `custom_agent_acp.py` - ACP server with a custom agent
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-w, --workspace` | Workspace root directory |
|
||||
| `-a, --agent` | Agent name or config file |
|
||||
| `-m, --model` | LLM model to use |
|
||||
| `-r, --resume` | Resume session by ID |
|
||||
| `--last` | Resume the last session |
|
||||
| `--approve` | Approval mode: manual, auto, scoped |
|
||||
| `--read-only` | Read-only mode (default) |
|
||||
| `--allow-write` | Allow file writes |
|
||||
| `--allow-shell` | Allow shell commands |
|
||||
| `--debug` | Enable debug logging |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install praisonai[acp]
|
||||
```
|
||||
|
||||
## Protocol Details
|
||||
|
||||
ACP uses JSON-RPC 2.0 over stdio:
|
||||
- **stdin**: Receives JSON-RPC requests from the client
|
||||
- **stdout**: Sends JSON-RPC responses to the client
|
||||
- **stderr**: Debug/log output (never pollutes stdout)
|
||||
|
||||
### Supported Methods
|
||||
|
||||
**Agent Methods (client → agent):**
|
||||
- `initialize` - Negotiate protocol version and capabilities
|
||||
- `authenticate` - Optional authentication
|
||||
- `session/new` - Create a new session
|
||||
- `session/load` - Load an existing session
|
||||
- `session/prompt` - Send user message
|
||||
- `session/cancel` - Cancel ongoing operations
|
||||
- `session/set_mode` - Change operating mode
|
||||
|
||||
**Client Methods (agent → client):**
|
||||
- `session/update` - Send progress updates
|
||||
- `session/request_permission` - Request user approval
|
||||
- `fs/read_text_file` - Read file contents
|
||||
- `fs/write_text_file` - Write file contents
|
||||
- `terminal/create` - Create terminal
|
||||
- `terminal/output` - Get terminal output
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Basic ACP Server Example
|
||||
|
||||
This example demonstrates how to run PraisonAI as an ACP server
|
||||
that can be connected to by IDEs like Zed, JetBrains, VSCode, or Toad.
|
||||
|
||||
Usage:
|
||||
# Run from command line
|
||||
praisonai acp
|
||||
|
||||
# Or run this script directly
|
||||
python basic_acp_server.py
|
||||
"""
|
||||
|
||||
from praisonai.acp import serve
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Start ACP server with default settings
|
||||
# This will listen on stdin/stdout for JSON-RPC messages
|
||||
serve(
|
||||
workspace=".", # Current directory as workspace
|
||||
agent="default", # Use default agent
|
||||
debug=False, # Set to True for debug logging to stderr
|
||||
read_only=True, # Safe by default
|
||||
approval_mode="manual", # Require approval for actions
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Custom Agent ACP Server Example
|
||||
|
||||
This example shows how to run a custom PraisonAI agent as an ACP server.
|
||||
|
||||
Usage:
|
||||
python custom_agent_acp.py
|
||||
"""
|
||||
|
||||
from praisonai.acp import serve, ACPServer, ACPConfig
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Create a custom agent
|
||||
agent = Agent(
|
||||
name="CodeAssistant",
|
||||
instructions="""You are an expert coding assistant. You help users:
|
||||
- Write clean, efficient code
|
||||
- Debug issues
|
||||
- Explain complex concepts
|
||||
- Review code for best practices
|
||||
|
||||
Always provide clear explanations with your code suggestions.""",
|
||||
model="gpt-4o-mini",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create config
|
||||
config = ACPConfig(
|
||||
workspace=".",
|
||||
debug=True,
|
||||
allow_write=True, # Allow file modifications
|
||||
approval_mode="manual",
|
||||
)
|
||||
|
||||
# Create server with custom agent
|
||||
server = ACPServer(config=config, agent=agent)
|
||||
|
||||
# Run the server
|
||||
serve(
|
||||
workspace=".",
|
||||
debug=True,
|
||||
allow_write=True,
|
||||
approval_mode="manual",
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
chainlit.md
|
||||
@@ -0,0 +1,137 @@
|
||||
# ACP (Agent Client Protocol) Examples
|
||||
|
||||
This directory contains examples for using PraisonAI with the Agent Client Protocol (ACP).
|
||||
|
||||
## What is ACP?
|
||||
|
||||
The Agent Client Protocol (ACP) is a standardized JSON-RPC 2.0 protocol that allows code editors and IDEs to communicate with AI coding agents. It enables seamless integration between PraisonAI and editors like:
|
||||
|
||||
- **Zed**
|
||||
- **JetBrains IDEs** (IntelliJ, PyCharm, WebStorm, etc.)
|
||||
- **VSCode** (via extensions)
|
||||
- **Toad**
|
||||
|
||||
## Quick Start
|
||||
|
||||
### CLI Usage
|
||||
|
||||
```bash
|
||||
# Start ACP server with defaults
|
||||
praisonai acp
|
||||
|
||||
# Enable debug logging
|
||||
praisonai acp --debug
|
||||
|
||||
# Allow file writes
|
||||
praisonai acp --allow-write
|
||||
|
||||
# Use a specific model
|
||||
praisonai acp -m gpt-4o
|
||||
|
||||
# Resume last session
|
||||
praisonai acp --resume --last
|
||||
```
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
from praisonai.acp import serve
|
||||
|
||||
serve(
|
||||
workspace=".",
|
||||
debug=True,
|
||||
allow_write=True,
|
||||
)
|
||||
```
|
||||
|
||||
## Editor Configuration
|
||||
|
||||
### Zed
|
||||
|
||||
Add to `~/.config/zed/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_servers": {
|
||||
"PraisonAI": {
|
||||
"command": "praisonai",
|
||||
"args": ["acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JetBrains
|
||||
|
||||
Add to `~/.jetbrains/acp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_servers": {
|
||||
"PraisonAI": {
|
||||
"command": "praisonai",
|
||||
"args": ["acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Toad
|
||||
|
||||
```bash
|
||||
toad acp "praisonai acp"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
- `basic_acp_server.py` - Simple ACP server with default settings
|
||||
- `custom_agent_acp.py` - ACP server with a custom agent
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `-w, --workspace` | Workspace root directory |
|
||||
| `-a, --agent` | Agent name or config file |
|
||||
| `-m, --model` | LLM model to use |
|
||||
| `-r, --resume` | Resume session by ID |
|
||||
| `--last` | Resume the last session |
|
||||
| `--approve` | Approval mode: manual, auto, scoped |
|
||||
| `--read-only` | Read-only mode (default) |
|
||||
| `--allow-write` | Allow file writes |
|
||||
| `--allow-shell` | Allow shell commands |
|
||||
| `--debug` | Enable debug logging |
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install praisonai[acp]
|
||||
```
|
||||
|
||||
## Protocol Details
|
||||
|
||||
ACP uses JSON-RPC 2.0 over stdio:
|
||||
- **stdin**: Receives JSON-RPC requests from the client
|
||||
- **stdout**: Sends JSON-RPC responses to the client
|
||||
- **stderr**: Debug/log output (never pollutes stdout)
|
||||
|
||||
### Supported Methods
|
||||
|
||||
**Agent Methods (client → agent):**
|
||||
- `initialize` - Negotiate protocol version and capabilities
|
||||
- `authenticate` - Optional authentication
|
||||
- `session/new` - Create a new session
|
||||
- `session/load` - Load an existing session
|
||||
- `session/prompt` - Send user message
|
||||
- `session/cancel` - Cancel ongoing operations
|
||||
- `session/set_mode` - Change operating mode
|
||||
|
||||
**Client Methods (agent → client):**
|
||||
- `session/update` - Send progress updates
|
||||
- `session/request_permission` - Request user approval
|
||||
- `fs/read_text_file` - Read file contents
|
||||
- `fs/write_text_file` - Write file contents
|
||||
- `terminal/create` - Create terminal
|
||||
- `terminal/output` - Get terminal output
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Basic ACP Server Example
|
||||
|
||||
This example demonstrates how to run PraisonAI as an ACP server
|
||||
that can be connected to by IDEs like Zed, JetBrains, VSCode, or Toad.
|
||||
|
||||
Usage:
|
||||
# Run from command line
|
||||
praisonai acp
|
||||
|
||||
# Or run this script directly
|
||||
python basic_acp_server.py
|
||||
"""
|
||||
|
||||
from praisonai.acp import serve
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Start ACP server with default settings
|
||||
# This will listen on stdin/stdout for JSON-RPC messages
|
||||
serve(
|
||||
workspace=".", # Current directory as workspace
|
||||
agent="default", # Use default agent
|
||||
debug=False, # Set to True for debug logging to stderr
|
||||
read_only=True, # Safe by default
|
||||
approval_mode="manual", # Require approval for actions
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Custom Agent ACP Server Example
|
||||
|
||||
This example shows how to run a custom PraisonAI agent as an ACP server.
|
||||
|
||||
Usage:
|
||||
python custom_agent_acp.py
|
||||
"""
|
||||
|
||||
from praisonai.acp import serve, ACPServer, ACPConfig
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Create a custom agent
|
||||
agent = Agent(
|
||||
name="CodeAssistant",
|
||||
instructions="""You are an expert coding assistant. You help users:
|
||||
- Write clean, efficient code
|
||||
- Debug issues
|
||||
- Explain complex concepts
|
||||
- Review code for best practices
|
||||
|
||||
Always provide clear explanations with your code suggestions.""",
|
||||
model="gpt-4o-mini",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create config
|
||||
config = ACPConfig(
|
||||
workspace=".",
|
||||
debug=True,
|
||||
allow_write=True, # Allow file modifications
|
||||
approval_mode="manual",
|
||||
)
|
||||
|
||||
# Create server with custom agent
|
||||
server = ACPServer(config=config, agent=agent)
|
||||
|
||||
# Run the server
|
||||
serve(
|
||||
workspace=".",
|
||||
debug=True,
|
||||
allow_write=True,
|
||||
approval_mode="manual",
|
||||
)
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.8.4.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.8.4.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.8.9.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.8.9.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Agent Client Protocol (ACP) support for PraisonAI.
|
||||
|
||||
This module provides ACP server functionality that allows IDEs/editors
|
||||
(Zed, JetBrains, VSCode, Toad) to connect to PraisonAI agents.
|
||||
|
||||
Usage:
|
||||
# CLI
|
||||
praisonai acp [OPTIONS]
|
||||
|
||||
# Python API
|
||||
from praisonai.acp import serve
|
||||
serve(workspace=".", agent="default")
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
|
||||
__all__ = [
|
||||
"serve",
|
||||
"ACPServer",
|
||||
"ACPSession",
|
||||
"ACPConfig",
|
||||
]
|
||||
|
||||
# Lazy imports for performance
|
||||
def __getattr__(name: str):
|
||||
"""Lazy import ACP components."""
|
||||
if name == "serve":
|
||||
from .server import serve
|
||||
return serve
|
||||
if name == "ACPServer":
|
||||
from .server import ACPServer
|
||||
return ACPServer
|
||||
if name == "ACPSession":
|
||||
from .session import ACPSession
|
||||
return ACPSession
|
||||
if name == "ACPConfig":
|
||||
from .config import ACPConfig
|
||||
return ACPConfig
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
def _check_acp_available() -> bool:
|
||||
"""Check if agent-client-protocol package is installed."""
|
||||
return importlib.util.find_spec("acp") is not None
|
||||
|
||||
|
||||
def _get_install_instructions() -> str:
|
||||
"""Get installation instructions for ACP support."""
|
||||
return (
|
||||
"ACP support requires the agent-client-protocol package.\n"
|
||||
"Install with: pip install praisonai[acp]\n"
|
||||
"Or: pip install agent-client-protocol"
|
||||
)
|
||||
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
ACP configuration management.
|
||||
|
||||
Handles configuration precedence:
|
||||
1. CLI flags (highest)
|
||||
2. Environment variables
|
||||
3. Config file (~/.praison/config.yaml)
|
||||
4. Defaults (lowest)
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ACPConfig:
|
||||
"""Configuration for ACP server."""
|
||||
|
||||
# Workspace settings
|
||||
workspace: Path = field(default_factory=Path.cwd)
|
||||
|
||||
# Agent settings
|
||||
agent: str = "default"
|
||||
agents_config: Optional[Path] = None
|
||||
router_enabled: bool = False
|
||||
model: Optional[str] = None
|
||||
|
||||
# Session settings
|
||||
resume_session: Optional[str] = None
|
||||
resume_last: bool = False
|
||||
|
||||
# Permission settings
|
||||
read_only: bool = True
|
||||
allow_write: bool = False
|
||||
allow_shell: bool = False
|
||||
allow_network: bool = False
|
||||
approval_mode: str = "manual" # manual, auto, scoped
|
||||
|
||||
# Workspace boundaries
|
||||
allowed_paths: List[Path] = field(default_factory=list)
|
||||
|
||||
# Debug settings
|
||||
debug: bool = False
|
||||
|
||||
# Profile
|
||||
profile: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate and normalize configuration."""
|
||||
if isinstance(self.workspace, str):
|
||||
self.workspace = Path(self.workspace).resolve()
|
||||
|
||||
if self.agents_config and isinstance(self.agents_config, str):
|
||||
self.agents_config = Path(self.agents_config)
|
||||
|
||||
# Set up default allowed paths
|
||||
if not self.allowed_paths:
|
||||
self.allowed_paths = [
|
||||
self.workspace,
|
||||
Path.home() / ".praison",
|
||||
]
|
||||
|
||||
# Normalize allowed paths
|
||||
self.allowed_paths = [
|
||||
Path(p).resolve() if isinstance(p, str) else p.resolve()
|
||||
for p in self.allowed_paths
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "ACPConfig":
|
||||
"""Create config from environment variables."""
|
||||
return cls(
|
||||
workspace=Path(os.environ.get("PRAISONAI_WORKSPACE", ".")).resolve(),
|
||||
agent=os.environ.get("PRAISONAI_AGENT", "default"),
|
||||
model=os.environ.get("PRAISONAI_MODEL") or os.environ.get("OPENAI_MODEL"),
|
||||
debug=os.environ.get("PRAISONAI_DEBUG", "").lower() in ("1", "true", "yes"),
|
||||
read_only=os.environ.get("PRAISONAI_READ_ONLY", "true").lower() in ("1", "true", "yes"),
|
||||
approval_mode=os.environ.get("PRAISONAI_APPROVAL_MODE", "manual"),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Optional[Path] = None) -> "ACPConfig":
|
||||
"""Load config from YAML file."""
|
||||
if path is None:
|
||||
path = Path.home() / ".praison" / "config.yaml"
|
||||
|
||||
if not path.exists():
|
||||
return cls()
|
||||
|
||||
try:
|
||||
import yaml
|
||||
with open(path) as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
|
||||
acp_config = data.get("acp", {})
|
||||
return cls(
|
||||
workspace=Path(acp_config.get("workspace", ".")).resolve(),
|
||||
agent=acp_config.get("agent", "default"),
|
||||
model=acp_config.get("model"),
|
||||
debug=acp_config.get("debug", False),
|
||||
read_only=acp_config.get("read_only", True),
|
||||
approval_mode=acp_config.get("approval_mode", "manual"),
|
||||
)
|
||||
except Exception:
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def merge(cls, *configs: "ACPConfig") -> "ACPConfig":
|
||||
"""Merge multiple configs, later configs override earlier ones."""
|
||||
result = cls()
|
||||
for config in configs:
|
||||
for field_name in [
|
||||
"workspace", "agent", "agents_config", "router_enabled",
|
||||
"model", "resume_session", "resume_last", "read_only",
|
||||
"allow_write", "allow_shell", "allow_network", "approval_mode",
|
||||
"debug", "profile"
|
||||
]:
|
||||
value = getattr(config, field_name)
|
||||
# Only override if value is not default/None
|
||||
if value is not None and value != getattr(cls(), field_name, None):
|
||||
setattr(result, field_name, value)
|
||||
return result
|
||||
|
||||
def is_path_allowed(self, path: Path) -> bool:
|
||||
"""Check if a path is within allowed boundaries."""
|
||||
path = Path(path).resolve()
|
||||
return any(
|
||||
path == allowed or allowed in path.parents
|
||||
for allowed in self.allowed_paths
|
||||
)
|
||||
|
||||
def can_write(self) -> bool:
|
||||
"""Check if write operations are allowed."""
|
||||
return not self.read_only or self.allow_write
|
||||
|
||||
def can_execute_shell(self) -> bool:
|
||||
"""Check if shell commands are allowed."""
|
||||
return self.allow_shell
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert config to dictionary."""
|
||||
return {
|
||||
"workspace": str(self.workspace),
|
||||
"agent": self.agent,
|
||||
"agents_config": str(self.agents_config) if self.agents_config else None,
|
||||
"router_enabled": self.router_enabled,
|
||||
"model": self.model,
|
||||
"resume_session": self.resume_session,
|
||||
"resume_last": self.resume_last,
|
||||
"read_only": self.read_only,
|
||||
"allow_write": self.allow_write,
|
||||
"allow_shell": self.allow_shell,
|
||||
"allow_network": self.allow_network,
|
||||
"approval_mode": self.approval_mode,
|
||||
"debug": self.debug,
|
||||
"profile": self.profile,
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
"""
|
||||
ACP Server implementation for PraisonAI.
|
||||
|
||||
This module implements the Agent Client Protocol server that allows
|
||||
IDEs/editors to communicate with PraisonAI agents.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from .config import ACPConfig
|
||||
from .session import ACPSession, SessionStore
|
||||
|
||||
# Configure logging to stderr only (stdout reserved for JSON-RPC)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _setup_stderr_logging(debug: bool = False) -> None:
|
||||
"""Configure logging to stderr only."""
|
||||
handler = logging.StreamHandler(sys.stderr)
|
||||
handler.setFormatter(logging.Formatter(
|
||||
"%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
))
|
||||
|
||||
root_logger = logging.getLogger("praisonai.acp")
|
||||
root_logger.handlers.clear()
|
||||
root_logger.addHandler(handler)
|
||||
root_logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
||||
|
||||
|
||||
class ACPServer:
|
||||
"""
|
||||
ACP Server that exposes PraisonAI agents to IDE clients.
|
||||
|
||||
Implements the Agent Client Protocol (JSON-RPC 2.0 over stdio).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[ACPConfig] = None,
|
||||
agent: Optional[Any] = None,
|
||||
agents: Optional[Any] = None,
|
||||
):
|
||||
"""
|
||||
Initialize ACP server.
|
||||
|
||||
Args:
|
||||
config: ACP configuration
|
||||
agent: Optional pre-configured Agent instance
|
||||
agents: Optional pre-configured PraisonAIAgents instance
|
||||
"""
|
||||
self.config = config or ACPConfig()
|
||||
self._agent = agent
|
||||
self._agents = agents
|
||||
self._session_store = SessionStore()
|
||||
self._sessions: Dict[str, ACPSession] = {}
|
||||
self._client = None # ACP client connection
|
||||
self._cancelled_sessions: set = set()
|
||||
|
||||
_setup_stderr_logging(self.config.debug)
|
||||
|
||||
def _get_agent(self):
|
||||
"""Lazy load or create agent."""
|
||||
if self._agent is not None:
|
||||
return self._agent
|
||||
|
||||
if self._agents is not None:
|
||||
return self._agents
|
||||
|
||||
# Create default agent
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
self._agent = Agent(
|
||||
name="PraisonAI",
|
||||
instructions="You are a helpful AI coding assistant.",
|
||||
model=self.config.model or "gpt-4o-mini",
|
||||
)
|
||||
return self._agent
|
||||
except ImportError:
|
||||
logger.error("praisonaiagents not installed")
|
||||
raise RuntimeError("praisonaiagents package required")
|
||||
|
||||
def on_connect(self, client) -> None:
|
||||
"""Called when client connects."""
|
||||
self._client = client
|
||||
logger.info("Client connected")
|
||||
|
||||
async def initialize(
|
||||
self,
|
||||
protocol_version: int,
|
||||
client_capabilities: Optional[Dict[str, Any]] = None,
|
||||
client_info: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Handle initialize request.
|
||||
|
||||
Negotiate protocol version and exchange capabilities.
|
||||
"""
|
||||
logger.info(f"Initialize: protocol_version={protocol_version}, client_info={client_info}")
|
||||
|
||||
# Import here to avoid circular imports and check availability
|
||||
try:
|
||||
from acp import PROTOCOL_VERSION
|
||||
except ImportError:
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
return {
|
||||
"protocolVersion": min(protocol_version, PROTOCOL_VERSION),
|
||||
"agentCapabilities": {
|
||||
"loadSession": True,
|
||||
"promptCapabilities": {
|
||||
"image": False,
|
||||
"audio": False,
|
||||
"embeddedContext": True,
|
||||
},
|
||||
"mcpCapabilities": {
|
||||
"http": False,
|
||||
"sse": False,
|
||||
},
|
||||
},
|
||||
"agentInfo": {
|
||||
"name": "praisonai",
|
||||
"title": "PraisonAI",
|
||||
"version": self._get_version(),
|
||||
},
|
||||
"authMethods": [],
|
||||
}
|
||||
|
||||
async def authenticate(self, method_id: str, **kwargs: Any) -> Optional[Dict[str, Any]]:
|
||||
"""Handle authenticate request (optional)."""
|
||||
logger.info(f"Authenticate: method_id={method_id}")
|
||||
return {}
|
||||
|
||||
async def new_session(
|
||||
self,
|
||||
cwd: str,
|
||||
mcp_servers: List[Dict[str, Any]],
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new conversation session.
|
||||
|
||||
Args:
|
||||
cwd: Working directory for the session
|
||||
mcp_servers: List of MCP server configurations
|
||||
"""
|
||||
logger.info(f"New session: cwd={cwd}")
|
||||
|
||||
workspace = Path(cwd).resolve()
|
||||
session = ACPSession.create(
|
||||
workspace=workspace,
|
||||
agent_id=self.config.agent,
|
||||
)
|
||||
session.mcp_servers = mcp_servers
|
||||
|
||||
self._sessions[session.session_id] = session
|
||||
self._session_store.save(session)
|
||||
|
||||
logger.info(f"Created session: {session.session_id}")
|
||||
|
||||
return {
|
||||
"sessionId": session.session_id,
|
||||
"modes": self._get_available_modes(),
|
||||
}
|
||||
|
||||
async def load_session(
|
||||
self,
|
||||
cwd: str,
|
||||
mcp_servers: List[Dict[str, Any]],
|
||||
session_id: str,
|
||||
**kwargs: Any,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Load an existing session.
|
||||
|
||||
Replays conversation history via session/update notifications.
|
||||
"""
|
||||
logger.info(f"Load session: session_id={session_id}")
|
||||
|
||||
# Try to load from store
|
||||
session = self._session_store.load(session_id)
|
||||
if session is None:
|
||||
logger.warning(f"Session not found: {session_id}")
|
||||
return None
|
||||
|
||||
# Update workspace and MCP servers
|
||||
session.workspace = Path(cwd).resolve()
|
||||
session.mcp_servers = mcp_servers
|
||||
session.update_activity()
|
||||
|
||||
self._sessions[session_id] = session
|
||||
|
||||
# Replay conversation history
|
||||
await self._replay_session_history(session)
|
||||
|
||||
return {}
|
||||
|
||||
async def list_sessions(
|
||||
self,
|
||||
cursor: Optional[str] = None,
|
||||
cwd: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""List available sessions."""
|
||||
sessions = self._session_store.list_sessions()
|
||||
|
||||
# Filter by cwd if provided
|
||||
if cwd:
|
||||
cwd_path = Path(cwd).resolve()
|
||||
sessions = [s for s in sessions if s.workspace == cwd_path]
|
||||
|
||||
return {
|
||||
"sessions": [
|
||||
{
|
||||
"sessionId": s.session_id,
|
||||
"createdAt": s.created_at,
|
||||
"lastActivity": s.last_activity,
|
||||
}
|
||||
for s in sessions
|
||||
],
|
||||
}
|
||||
|
||||
async def set_session_mode(
|
||||
self,
|
||||
mode_id: str,
|
||||
session_id: str,
|
||||
**kwargs: Any,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Set the operating mode for a session."""
|
||||
logger.info(f"Set session mode: session_id={session_id}, mode_id={mode_id}")
|
||||
|
||||
session = self._sessions.get(session_id)
|
||||
if session:
|
||||
session.mode = mode_id
|
||||
self._session_store.save(session)
|
||||
|
||||
return {}
|
||||
|
||||
async def set_session_model(
|
||||
self,
|
||||
model_id: str,
|
||||
session_id: str,
|
||||
**kwargs: Any,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Set the LLM model for a session."""
|
||||
logger.info(f"Set session model: session_id={session_id}, model_id={model_id}")
|
||||
|
||||
session = self._sessions.get(session_id)
|
||||
if session:
|
||||
session.model = model_id
|
||||
self._session_store.save(session)
|
||||
|
||||
return {}
|
||||
|
||||
async def prompt(
|
||||
self,
|
||||
prompt: List[Dict[str, Any]],
|
||||
session_id: str,
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Handle user prompt.
|
||||
|
||||
This is the main interaction method where the user sends a message
|
||||
and the agent processes it.
|
||||
"""
|
||||
logger.info(f"Prompt: session_id={session_id}")
|
||||
|
||||
session = self._sessions.get(session_id)
|
||||
if session is None:
|
||||
logger.error(f"Session not found: {session_id}")
|
||||
return {"stopReason": "refusal"}
|
||||
|
||||
# Check if cancelled
|
||||
if session_id in self._cancelled_sessions:
|
||||
self._cancelled_sessions.discard(session_id)
|
||||
return {"stopReason": "cancelled"}
|
||||
|
||||
# Extract text from prompt blocks
|
||||
user_message = self._extract_prompt_text(prompt)
|
||||
session.add_message("user", user_message)
|
||||
|
||||
try:
|
||||
# Get agent response
|
||||
agent = self._get_agent()
|
||||
|
||||
# Send thinking update
|
||||
await self._send_thought(session_id, "Processing your request...")
|
||||
|
||||
# Generate response
|
||||
response = await self._generate_response(agent, user_message, session)
|
||||
|
||||
# Send response as agent message chunks
|
||||
await self._send_agent_message(session_id, response)
|
||||
|
||||
# Save session
|
||||
session.add_message("assistant", response)
|
||||
self._session_store.save(session)
|
||||
|
||||
return {"stopReason": "end_turn"}
|
||||
|
||||
except asyncio.CancelledError:
|
||||
return {"stopReason": "cancelled"}
|
||||
except Exception as e:
|
||||
logger.exception(f"Error processing prompt: {e}")
|
||||
await self._send_agent_message(session_id, f"Error: {str(e)}")
|
||||
return {"stopReason": "refusal"}
|
||||
|
||||
async def fork_session(
|
||||
self,
|
||||
cwd: str,
|
||||
session_id: str,
|
||||
mcp_servers: Optional[List[Dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""Fork an existing session."""
|
||||
logger.info(f"Fork session: session_id={session_id}")
|
||||
|
||||
original = self._sessions.get(session_id)
|
||||
if original is None:
|
||||
original = self._session_store.load(session_id)
|
||||
|
||||
if original is None:
|
||||
# Create new session if original not found
|
||||
return await self.new_session(cwd, mcp_servers or [])
|
||||
|
||||
# Create forked session
|
||||
forked = ACPSession.create(
|
||||
workspace=Path(cwd).resolve(),
|
||||
agent_id=original.agent_id,
|
||||
)
|
||||
forked.messages = original.messages.copy()
|
||||
forked.mcp_servers = mcp_servers or original.mcp_servers
|
||||
|
||||
self._sessions[forked.session_id] = forked
|
||||
self._session_store.save(forked)
|
||||
|
||||
return {"sessionId": forked.session_id}
|
||||
|
||||
async def resume_session(
|
||||
self,
|
||||
cwd: str,
|
||||
session_id: str,
|
||||
mcp_servers: Optional[List[Dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""Resume an existing session."""
|
||||
return await self.load_session(cwd, mcp_servers or [], session_id)
|
||||
|
||||
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
||||
"""Cancel ongoing operations for a session."""
|
||||
logger.info(f"Cancel: session_id={session_id}")
|
||||
self._cancelled_sessions.add(session_id)
|
||||
|
||||
async def ext_method(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Handle extension method calls."""
|
||||
logger.info(f"Extension method: {method}")
|
||||
return {"error": f"Unknown extension method: {method}"}
|
||||
|
||||
async def ext_notification(self, method: str, params: Dict[str, Any]) -> None:
|
||||
"""Handle extension notifications."""
|
||||
logger.info(f"Extension notification: {method}")
|
||||
|
||||
# Helper methods
|
||||
|
||||
def _get_version(self) -> str:
|
||||
"""Get PraisonAI version."""
|
||||
try:
|
||||
from praisonai.version import __version__
|
||||
return __version__
|
||||
except ImportError:
|
||||
return "0.0.0"
|
||||
|
||||
def _get_available_modes(self) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get available operating modes."""
|
||||
return [
|
||||
{
|
||||
"id": "manual",
|
||||
"name": "Manual",
|
||||
"description": "Requires approval for all actions",
|
||||
},
|
||||
{
|
||||
"id": "auto",
|
||||
"name": "Auto",
|
||||
"description": "Auto-approve within workspace",
|
||||
},
|
||||
]
|
||||
|
||||
def _extract_prompt_text(self, prompt: List[Dict[str, Any]]) -> str:
|
||||
"""Extract text content from prompt blocks."""
|
||||
texts = []
|
||||
for block in prompt:
|
||||
if isinstance(block, dict):
|
||||
block_type = block.get("type", "")
|
||||
if block_type == "text":
|
||||
texts.append(block.get("text", ""))
|
||||
elif block_type == "resource":
|
||||
resource = block.get("resource", {})
|
||||
if "text" in resource:
|
||||
texts.append(f"[File: {resource.get('uri', 'unknown')}]\n{resource['text']}")
|
||||
return "\n".join(texts)
|
||||
|
||||
async def _generate_response(
|
||||
self,
|
||||
agent: Any,
|
||||
message: str,
|
||||
session: ACPSession,
|
||||
) -> str:
|
||||
"""Generate agent response."""
|
||||
try:
|
||||
# Try async chat first
|
||||
if hasattr(agent, "achat"):
|
||||
response = await agent.achat(message)
|
||||
elif hasattr(agent, "chat"):
|
||||
# Run sync chat in executor
|
||||
loop = asyncio.get_event_loop()
|
||||
response = await loop.run_in_executor(None, agent.chat, message)
|
||||
else:
|
||||
response = "Agent does not support chat interface"
|
||||
|
||||
return str(response) if response else ""
|
||||
except Exception as e:
|
||||
logger.exception(f"Error generating response: {e}")
|
||||
return f"Error generating response: {str(e)}"
|
||||
|
||||
async def _send_agent_message(self, session_id: str, text: str) -> None:
|
||||
"""Send agent message chunk to client."""
|
||||
if self._client is None:
|
||||
return
|
||||
|
||||
try:
|
||||
from acp import update_agent_message_text
|
||||
update = update_agent_message_text(text)
|
||||
await self._client.session_update(session_id=session_id, update=update)
|
||||
except ImportError:
|
||||
# Fallback without ACP SDK
|
||||
logger.debug(f"Agent message: {text[:100]}...")
|
||||
|
||||
async def _send_thought(self, session_id: str, text: str) -> None:
|
||||
"""Send agent thought to client."""
|
||||
if self._client is None:
|
||||
return
|
||||
|
||||
try:
|
||||
from acp import update_agent_thought_text
|
||||
update = update_agent_thought_text(text)
|
||||
await self._client.session_update(session_id=session_id, update=update)
|
||||
except ImportError:
|
||||
logger.debug(f"Agent thought: {text}")
|
||||
|
||||
async def _replay_session_history(self, session: ACPSession) -> None:
|
||||
"""Replay session history to client."""
|
||||
if self._client is None:
|
||||
return
|
||||
|
||||
try:
|
||||
from acp import update_user_message_text, update_agent_message_text
|
||||
|
||||
for msg in session.messages:
|
||||
role = msg.get("role", "")
|
||||
content = msg.get("content", "")
|
||||
|
||||
if role == "user":
|
||||
update = update_user_message_text(str(content))
|
||||
elif role == "assistant":
|
||||
update = update_agent_message_text(str(content))
|
||||
else:
|
||||
continue
|
||||
|
||||
await self._client.session_update(
|
||||
session_id=session.session_id,
|
||||
update=update,
|
||||
)
|
||||
except ImportError:
|
||||
logger.debug("Skipping history replay (ACP SDK not available)")
|
||||
|
||||
|
||||
async def _run_server(config: ACPConfig) -> None:
|
||||
"""Run the ACP server."""
|
||||
try:
|
||||
from acp import Agent as ACPAgent, run_agent
|
||||
except ImportError:
|
||||
logger.error(
|
||||
"agent-client-protocol package not installed.\n"
|
||||
"Install with: pip install praisonai[acp]\n"
|
||||
"Or: pip install agent-client-protocol"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
server = ACPServer(config=config)
|
||||
|
||||
# Create ACP-compatible agent wrapper
|
||||
class PraisonACPAgent(ACPAgent):
|
||||
def on_connect(self, conn):
|
||||
server.on_connect(conn)
|
||||
|
||||
async def initialize(self, protocol_version, client_capabilities=None, client_info=None, **kwargs):
|
||||
return await server.initialize(protocol_version, client_capabilities, client_info, **kwargs)
|
||||
|
||||
async def authenticate(self, method_id, **kwargs):
|
||||
return await server.authenticate(method_id, **kwargs)
|
||||
|
||||
async def new_session(self, cwd, mcp_servers, **kwargs):
|
||||
return await server.new_session(cwd, mcp_servers, **kwargs)
|
||||
|
||||
async def load_session(self, cwd, mcp_servers, session_id, **kwargs):
|
||||
return await server.load_session(cwd, mcp_servers, session_id, **kwargs)
|
||||
|
||||
async def list_sessions(self, cursor=None, cwd=None, **kwargs):
|
||||
return await server.list_sessions(cursor, cwd, **kwargs)
|
||||
|
||||
async def set_session_mode(self, mode_id, session_id, **kwargs):
|
||||
return await server.set_session_mode(mode_id, session_id, **kwargs)
|
||||
|
||||
async def set_session_model(self, model_id, session_id, **kwargs):
|
||||
return await server.set_session_model(model_id, session_id, **kwargs)
|
||||
|
||||
async def prompt(self, prompt, session_id, **kwargs):
|
||||
return await server.prompt(prompt, session_id, **kwargs)
|
||||
|
||||
async def fork_session(self, cwd, session_id, mcp_servers=None, **kwargs):
|
||||
return await server.fork_session(cwd, session_id, mcp_servers, **kwargs)
|
||||
|
||||
async def resume_session(self, cwd, session_id, mcp_servers=None, **kwargs):
|
||||
return await server.resume_session(cwd, session_id, mcp_servers, **kwargs)
|
||||
|
||||
async def cancel(self, session_id, **kwargs):
|
||||
return await server.cancel(session_id, **kwargs)
|
||||
|
||||
async def ext_method(self, method, params):
|
||||
return await server.ext_method(method, params)
|
||||
|
||||
async def ext_notification(self, method, params):
|
||||
return await server.ext_notification(method, params)
|
||||
|
||||
logger.info("Starting PraisonAI ACP server...")
|
||||
await run_agent(PraisonACPAgent(), use_unstable_protocol=True)
|
||||
|
||||
|
||||
def serve(
|
||||
workspace: Union[str, Path] = ".",
|
||||
agent: str = "default",
|
||||
model: Optional[str] = None,
|
||||
resume: Optional[str] = None,
|
||||
resume_last: bool = False,
|
||||
debug: bool = False,
|
||||
read_only: bool = True,
|
||||
allow_write: bool = False,
|
||||
allow_shell: bool = False,
|
||||
approval_mode: str = "manual",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
Start the ACP server.
|
||||
|
||||
This is the main entry point for running PraisonAI as an ACP agent.
|
||||
|
||||
Args:
|
||||
workspace: Working directory for the session
|
||||
agent: Agent name or configuration
|
||||
model: LLM model to use
|
||||
resume: Session ID to resume
|
||||
resume_last: Resume the last session
|
||||
debug: Enable debug logging
|
||||
read_only: Read-only mode (default True)
|
||||
allow_write: Allow file writes
|
||||
allow_shell: Allow shell commands
|
||||
approval_mode: Approval mode (manual, auto, scoped)
|
||||
"""
|
||||
config = ACPConfig(
|
||||
workspace=Path(workspace).resolve(),
|
||||
agent=agent,
|
||||
model=model,
|
||||
resume_session=resume,
|
||||
resume_last=resume_last,
|
||||
debug=debug,
|
||||
read_only=read_only,
|
||||
allow_write=allow_write,
|
||||
allow_shell=allow_shell,
|
||||
approval_mode=approval_mode,
|
||||
)
|
||||
|
||||
asyncio.run(_run_server(config))
|
||||
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
ACP session management.
|
||||
|
||||
Handles session creation, persistence, and resume functionality.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ACPSession:
|
||||
"""Represents an ACP conversation session."""
|
||||
|
||||
session_id: str
|
||||
workspace: Path
|
||||
created_at: float = field(default_factory=time.time)
|
||||
last_activity: float = field(default_factory=time.time)
|
||||
|
||||
# Agent attribution
|
||||
agent_id: Optional[str] = None
|
||||
run_id: Optional[str] = None
|
||||
trace_id: Optional[str] = None
|
||||
|
||||
# Session state
|
||||
mode: str = "manual" # manual, auto, full_auto
|
||||
model: Optional[str] = None
|
||||
|
||||
# Conversation history (for resume)
|
||||
messages: List[Dict[str, Any]] = field(default_factory=list)
|
||||
tool_calls: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
# MCP servers
|
||||
mcp_servers: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def create(cls, workspace: Path, agent_id: Optional[str] = None) -> "ACPSession":
|
||||
"""Create a new session."""
|
||||
session_id = f"sess_{uuid.uuid4().hex[:16]}"
|
||||
return cls(
|
||||
session_id=session_id,
|
||||
workspace=workspace,
|
||||
agent_id=agent_id,
|
||||
run_id=f"run_{uuid.uuid4().hex[:8]}",
|
||||
trace_id=f"trace_{uuid.uuid4().hex[:8]}",
|
||||
)
|
||||
|
||||
def update_activity(self) -> None:
|
||||
"""Update last activity timestamp."""
|
||||
self.last_activity = time.time()
|
||||
|
||||
def add_message(self, role: str, content: Any) -> None:
|
||||
"""Add a message to the conversation history."""
|
||||
self.messages.append({
|
||||
"role": role,
|
||||
"content": content,
|
||||
"timestamp": time.time(),
|
||||
})
|
||||
self.update_activity()
|
||||
|
||||
def add_tool_call(self, tool_call_id: str, title: str, status: str, **kwargs) -> None:
|
||||
"""Add a tool call to the history."""
|
||||
self.tool_calls.append({
|
||||
"tool_call_id": tool_call_id,
|
||||
"title": title,
|
||||
"status": status,
|
||||
"timestamp": time.time(),
|
||||
**kwargs,
|
||||
})
|
||||
self.update_activity()
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize session to dictionary."""
|
||||
return {
|
||||
"session_id": self.session_id,
|
||||
"workspace": str(self.workspace),
|
||||
"created_at": self.created_at,
|
||||
"last_activity": self.last_activity,
|
||||
"agent_id": self.agent_id,
|
||||
"run_id": self.run_id,
|
||||
"trace_id": self.trace_id,
|
||||
"mode": self.mode,
|
||||
"model": self.model,
|
||||
"messages": self.messages,
|
||||
"tool_calls": self.tool_calls,
|
||||
"mcp_servers": self.mcp_servers,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ACPSession":
|
||||
"""Deserialize session from dictionary."""
|
||||
return cls(
|
||||
session_id=data["session_id"],
|
||||
workspace=Path(data["workspace"]),
|
||||
created_at=data.get("created_at", time.time()),
|
||||
last_activity=data.get("last_activity", time.time()),
|
||||
agent_id=data.get("agent_id"),
|
||||
run_id=data.get("run_id"),
|
||||
trace_id=data.get("trace_id"),
|
||||
mode=data.get("mode", "manual"),
|
||||
model=data.get("model"),
|
||||
messages=data.get("messages", []),
|
||||
tool_calls=data.get("tool_calls", []),
|
||||
mcp_servers=data.get("mcp_servers", []),
|
||||
)
|
||||
|
||||
|
||||
class SessionStore:
|
||||
"""Persistent storage for ACP sessions."""
|
||||
|
||||
def __init__(self, storage_dir: Optional[Path] = None):
|
||||
"""Initialize session store."""
|
||||
if storage_dir is None:
|
||||
storage_dir = Path.home() / ".praison" / "acp" / "sessions"
|
||||
self.storage_dir = storage_dir
|
||||
self.storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._last_session_file = self.storage_dir / ".last_session"
|
||||
|
||||
def _session_path(self, session_id: str) -> Path:
|
||||
"""Get path for session file."""
|
||||
return self.storage_dir / f"{session_id}.json"
|
||||
|
||||
def save(self, session: ACPSession) -> None:
|
||||
"""Save session to disk."""
|
||||
try:
|
||||
path = self._session_path(session.session_id)
|
||||
with open(path, "w") as f:
|
||||
json.dump(session.to_dict(), f, indent=2)
|
||||
|
||||
# Update last session pointer
|
||||
with open(self._last_session_file, "w") as f:
|
||||
f.write(session.session_id)
|
||||
|
||||
logger.debug(f"Saved session {session.session_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save session: {e}")
|
||||
|
||||
def load(self, session_id: str) -> Optional[ACPSession]:
|
||||
"""Load session from disk."""
|
||||
try:
|
||||
path = self._session_path(session_id)
|
||||
if not path.exists():
|
||||
logger.warning(f"Session not found: {session_id}")
|
||||
return None
|
||||
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
return ACPSession.from_dict(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load session: {e}")
|
||||
return None
|
||||
|
||||
def load_last(self) -> Optional[ACPSession]:
|
||||
"""Load the most recent session."""
|
||||
try:
|
||||
if not self._last_session_file.exists():
|
||||
return None
|
||||
|
||||
with open(self._last_session_file) as f:
|
||||
session_id = f.read().strip()
|
||||
|
||||
if session_id:
|
||||
return self.load(session_id)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load last session: {e}")
|
||||
return None
|
||||
|
||||
def delete(self, session_id: str) -> bool:
|
||||
"""Delete a session."""
|
||||
try:
|
||||
path = self._session_path(session_id)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
logger.debug(f"Deleted session {session_id}")
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete session: {e}")
|
||||
return False
|
||||
|
||||
def list_sessions(self, limit: int = 50) -> List[ACPSession]:
|
||||
"""List all sessions, sorted by last activity."""
|
||||
sessions = []
|
||||
try:
|
||||
for path in self.storage_dir.glob("sess_*.json"):
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
sessions.append(ACPSession.from_dict(data))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Sort by last activity, most recent first
|
||||
sessions.sort(key=lambda s: s.last_activity, reverse=True)
|
||||
return sessions[:limit]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list sessions: {e}")
|
||||
return []
|
||||
|
||||
def cleanup_old_sessions(self, max_age_days: int = 30) -> int:
|
||||
"""Remove sessions older than max_age_days."""
|
||||
cutoff = time.time() - (max_age_days * 24 * 60 * 60)
|
||||
removed = 0
|
||||
|
||||
for session in self.list_sessions(limit=1000):
|
||||
if session.last_activity < cutoff:
|
||||
if self.delete(session.session_id):
|
||||
removed += 1
|
||||
|
||||
return removed
|
||||
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
ACP CLI handler for PraisonAI.
|
||||
|
||||
Provides the `praisonai acp` command for IDE/editor integration.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ACPHandler:
|
||||
"""Handler for ACP CLI commands."""
|
||||
|
||||
@staticmethod
|
||||
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
"""Add ACP-specific arguments to parser."""
|
||||
parser.add_argument(
|
||||
"-w", "--workspace",
|
||||
type=str,
|
||||
default=".",
|
||||
help="Workspace root directory (default: current directory)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a", "--agent",
|
||||
type=str,
|
||||
default="default",
|
||||
help="Agent name or configuration file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--agents",
|
||||
type=str,
|
||||
help="Multi-agent configuration YAML file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--router",
|
||||
action="store_true",
|
||||
help="Enable router agent for task delegation"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m", "--model",
|
||||
type=str,
|
||||
help="LLM model to use"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r", "--resume",
|
||||
type=str,
|
||||
nargs="?",
|
||||
const="__last__",
|
||||
help="Resume session by ID (or --resume --last for most recent)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--last",
|
||||
action="store_true",
|
||||
help="Resume the last session (use with --resume)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--approve",
|
||||
type=str,
|
||||
choices=["manual", "auto", "scoped"],
|
||||
default="manual",
|
||||
help="Approval mode for actions (default: manual)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--read-only",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Read-only mode (default: enabled)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-write",
|
||||
action="store_true",
|
||||
help="Allow file write operations"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-shell",
|
||||
action="store_true",
|
||||
help="Allow shell command execution"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-network",
|
||||
action="store_true",
|
||||
help="Allow network requests"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
help="Enable debug logging to stderr"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--profile",
|
||||
type=str,
|
||||
help="Use named profile from config"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def handle(args: argparse.Namespace) -> int:
|
||||
"""
|
||||
Handle ACP command execution.
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, non-zero for error)
|
||||
"""
|
||||
# Check if ACP SDK is available
|
||||
try:
|
||||
import importlib.util
|
||||
if importlib.util.find_spec("acp") is None:
|
||||
print(
|
||||
"Error: agent-client-protocol package not installed.\n"
|
||||
"Install with: pip install praisonai[acp]\n"
|
||||
"Or: pip install agent-client-protocol",
|
||||
file=sys.stderr
|
||||
)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error checking ACP availability: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Import ACP module
|
||||
try:
|
||||
from praisonai.acp import serve
|
||||
except ImportError as e:
|
||||
print(f"Error importing ACP module: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Determine resume session
|
||||
resume_session: Optional[str] = None
|
||||
resume_last = False
|
||||
|
||||
if args.resume:
|
||||
if args.resume == "__last__" or args.last:
|
||||
resume_last = True
|
||||
else:
|
||||
resume_session = args.resume
|
||||
elif args.last:
|
||||
resume_last = True
|
||||
|
||||
# Determine read-only mode
|
||||
read_only = args.read_only and not args.allow_write
|
||||
|
||||
try:
|
||||
# Start ACP server
|
||||
serve(
|
||||
workspace=args.workspace,
|
||||
agent=args.agent,
|
||||
model=args.model,
|
||||
resume=resume_session,
|
||||
resume_last=resume_last,
|
||||
debug=args.debug,
|
||||
read_only=read_only,
|
||||
allow_write=args.allow_write,
|
||||
allow_shell=args.allow_shell,
|
||||
approval_mode=args.approve,
|
||||
)
|
||||
return 0
|
||||
except KeyboardInterrupt:
|
||||
logger.info("ACP server stopped by user")
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.exception(f"ACP server error: {e}")
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def setup_acp_subparser(subparsers) -> None:
|
||||
"""Set up the ACP subcommand parser."""
|
||||
acp_parser = subparsers.add_parser(
|
||||
"acp",
|
||||
help="Start ACP server for IDE/editor integration",
|
||||
description=(
|
||||
"Start the Agent Client Protocol (ACP) server.\n\n"
|
||||
"This allows IDEs and editors (Zed, JetBrains, VSCode, Toad) "
|
||||
"to connect to PraisonAI agents via JSON-RPC over stdio."
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
praisonai acp # Start with defaults
|
||||
praisonai acp --debug # Enable debug logging
|
||||
praisonai acp --resume --last # Resume last session
|
||||
praisonai acp --allow-write # Enable file writes
|
||||
praisonai acp -m gpt-4o # Use specific model
|
||||
|
||||
Editor Configuration:
|
||||
|
||||
Zed (~/.config/zed/settings.json):
|
||||
{
|
||||
"agent_servers": {
|
||||
"PraisonAI": {
|
||||
"command": "praisonai",
|
||||
"args": ["acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JetBrains (~/.jetbrains/acp.json):
|
||||
{
|
||||
"agent_servers": {
|
||||
"PraisonAI": {
|
||||
"command": "praisonai",
|
||||
"args": ["acp"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toad:
|
||||
toad acp "praisonai acp"
|
||||
"""
|
||||
)
|
||||
ACPHandler.add_arguments(acp_parser)
|
||||
acp_parser.set_defaults(func=lambda args: ACPHandler.handle(args))
|
||||
|
||||
|
||||
def run_acp_command(args: Optional[list] = None) -> int:
|
||||
"""
|
||||
Run ACP command directly.
|
||||
|
||||
This can be called from the main CLI or as a standalone entry point.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="praisonai acp",
|
||||
description="Start ACP server for IDE/editor integration"
|
||||
)
|
||||
ACPHandler.add_arguments(parser)
|
||||
|
||||
parsed_args = parser.parse_args(args)
|
||||
return ACPHandler.handle(parsed_args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_acp_command())
|
||||
@@ -698,7 +698,7 @@ class PraisonAI:
|
||||
return default_args
|
||||
|
||||
# Define special commands
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output', 'deploy', 'templates', 'recipe', 'endpoints', 'audio', 'embed', 'images', 'moderate', 'files', 'batches', 'vector-stores', 'rerank', 'ocr', 'assistants', 'fine-tuning', 'completions', 'messages', 'guardrails', 'rag', 'videos', 'a2a', 'containers', 'passthrough', 'responses', 'search', 'realtime-api', 'doctor', 'registry', 'package', 'install', 'uninstall']
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output', 'deploy', 'templates', 'recipe', 'endpoints', 'audio', 'embed', 'images', 'moderate', 'files', 'batches', 'vector-stores', 'rerank', 'ocr', 'assistants', 'fine-tuning', 'completions', 'messages', 'guardrails', 'rag', 'videos', 'a2a', 'containers', 'passthrough', 'responses', 'search', 'realtime-api', 'doctor', 'registry', 'package', 'install', 'uninstall', 'acp']
|
||||
|
||||
parser = argparse.ArgumentParser(prog="praisonai", description="praisonAI command-line interface")
|
||||
parser.add_argument("--framework", choices=["crewai", "autogen", "praisonai"], help="Specify the framework")
|
||||
@@ -898,6 +898,12 @@ class PraisonAI:
|
||||
self._handle_serve_command(args, unknown_args)
|
||||
sys.exit(0)
|
||||
|
||||
# Handle ACP command - Agent Client Protocol server for IDE integration
|
||||
if args.command == 'acp':
|
||||
from praisonai.cli.features.acp import run_acp_command
|
||||
exit_code = run_acp_command(unknown_args)
|
||||
sys.exit(exit_code)
|
||||
|
||||
# Handle both command and flag versions for call
|
||||
if args.command == 'call' or args.call:
|
||||
if not CALL_MODULE_AVAILABLE:
|
||||
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==2.8.4 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.8.9 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -231,24 +231,16 @@ expected_password = os.getenv("CHAINLIT_PASSWORD", "admin")
|
||||
if expected_username == "admin" and expected_password == "admin":
|
||||
logger.warning("⚠️ Using default admin credentials. Set CHAINLIT_USERNAME and CHAINLIT_PASSWORD environment variables for production.")
|
||||
|
||||
def auth_callback(u: str, p: str):
|
||||
if (u, p) == (expected_username, expected_password):
|
||||
return cl.User(identifier=expected_username, metadata={"role": "ADMIN", "provider": "credentials"})
|
||||
return None
|
||||
@cl.password_auth_callback
|
||||
def auth_callback(username: str, password: str):
|
||||
logger.debug(f"Auth attempt: username='{username}', expected='{expected_username}'")
|
||||
if (username, password) == (expected_username, expected_password):
|
||||
logger.info(f"Login successful for user: {username}")
|
||||
return cl.User(identifier=username, metadata={"role": "admin", "provider": "credentials"})
|
||||
else:
|
||||
logger.warning(f"Login failed for user: {username}")
|
||||
return None
|
||||
|
||||
def oauth_callback(
|
||||
provider_id: str,
|
||||
token: str,
|
||||
raw_user_data: Dict[str, str],
|
||||
default_user: cl.User,
|
||||
) -> Optional[cl.User]:
|
||||
return default_user
|
||||
|
||||
if AUTH_PASSWORD_ENABLED:
|
||||
auth_callback = cl.password_auth_callback(auth_callback)
|
||||
|
||||
if AUTH_OAUTH_ENABLED:
|
||||
oauth_callback = cl.oauth_callback(oauth_callback)
|
||||
|
||||
async def send_count():
|
||||
await cl.Message(
|
||||
@@ -327,7 +319,6 @@ User Question: {message.content}
|
||||
|
||||
message_history.append({"role": "user", "content": user_message})
|
||||
msg = cl.Message(content="")
|
||||
await msg.send()
|
||||
|
||||
completion_params = {
|
||||
"model": model_name,
|
||||
@@ -357,6 +348,7 @@ User Question: {message.content}
|
||||
full_response = ""
|
||||
tool_calls = []
|
||||
current_tool_call = None
|
||||
msg_sent = False
|
||||
|
||||
async for part in response:
|
||||
if 'choices' in part and len(part['choices']) > 0:
|
||||
@@ -364,6 +356,10 @@ User Question: {message.content}
|
||||
|
||||
if 'content' in delta and delta['content'] is not None:
|
||||
token = delta['content']
|
||||
# Send message on first token to avoid delay
|
||||
if not msg_sent:
|
||||
await msg.send()
|
||||
msg_sent = True
|
||||
await msg.stream_token(token)
|
||||
full_response += token
|
||||
|
||||
@@ -390,6 +386,10 @@ User Question: {message.content}
|
||||
if current_tool_call:
|
||||
tool_calls.append(current_tool_call)
|
||||
|
||||
# Ensure message is sent even if no content (e.g., tool calls only)
|
||||
if not msg_sent:
|
||||
await msg.send()
|
||||
|
||||
logger.debug(f"Full response: {full_response}")
|
||||
logger.debug(f"Tool calls: {tool_calls}")
|
||||
message_history.append({"role": "assistant", "content": full_response})
|
||||
|
||||
@@ -398,7 +398,6 @@ Context:
|
||||
message_history.append({"role": "user", "content": user_message})
|
||||
|
||||
msg = cl.Message(content="")
|
||||
await msg.send()
|
||||
|
||||
# Use PraisonAI Agents if available, otherwise fallback to litellm
|
||||
if PRAISONAI_AGENTS_AVAILABLE:
|
||||
@@ -433,41 +432,47 @@ Then use the Claude Code tool to handle those requests.
|
||||
|
||||
For informational questions, explanations, or general conversations, respond normally without using Claude Code."""
|
||||
|
||||
# Create agent
|
||||
# Create agent with streaming enabled
|
||||
agent = Agent(
|
||||
name="PraisonAI Assistant",
|
||||
instructions=instructions,
|
||||
llm=model_name,
|
||||
tools=available_tools if available_tools else None
|
||||
tools=available_tools if available_tools else None,
|
||||
stream=True
|
||||
)
|
||||
|
||||
# Execute agent with streaming
|
||||
full_response = ""
|
||||
msg_sent = False
|
||||
|
||||
# Use agent's streaming capabilities if available
|
||||
try:
|
||||
# For now, use synchronous execution and stream the result
|
||||
# TODO: Implement proper streaming when PraisonAI agents support it
|
||||
result = agent.start(user_message)
|
||||
# Use async chat for proper streaming
|
||||
result = await agent.achat(user_message)
|
||||
|
||||
# Stream the response character by character for better UX
|
||||
# Get the response text
|
||||
if hasattr(result, 'raw'):
|
||||
response_text = result.raw
|
||||
else:
|
||||
response_text = str(result)
|
||||
|
||||
for char in response_text:
|
||||
await msg.stream_token(char)
|
||||
full_response += char
|
||||
# Small delay to make streaming visible
|
||||
await asyncio.sleep(0.01)
|
||||
# Send message on first content
|
||||
if not msg_sent:
|
||||
await msg.send()
|
||||
msg_sent = True
|
||||
|
||||
# Stream in word chunks for better UX (not char-by-char which is too slow)
|
||||
words = response_text.split(' ')
|
||||
for i, word in enumerate(words):
|
||||
token = word + (' ' if i < len(words) - 1 else '')
|
||||
await msg.stream_token(token)
|
||||
full_response += token
|
||||
|
||||
except Exception as e:
|
||||
error_response = f"Error executing agent: {str(e)}"
|
||||
for char in error_response:
|
||||
await msg.stream_token(char)
|
||||
full_response += char
|
||||
await asyncio.sleep(0.01)
|
||||
if not msg_sent:
|
||||
await msg.send()
|
||||
await msg.stream_token(error_response)
|
||||
full_response = error_response
|
||||
|
||||
msg.content = full_response
|
||||
await msg.update()
|
||||
@@ -475,6 +480,7 @@ For informational questions, explanations, or general conversations, respond nor
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to use PraisonAI Agents: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
await msg.send()
|
||||
await msg.stream_token(error_msg)
|
||||
msg.content = error_msg
|
||||
await msg.update()
|
||||
@@ -515,6 +521,7 @@ async def handle_with_litellm(user_message, model_name, message_history, msg, im
|
||||
full_response = ""
|
||||
tool_calls = []
|
||||
current_tool_call = None
|
||||
msg_sent = False
|
||||
|
||||
async for part in response:
|
||||
logger.debug(f"LLM part: {part}")
|
||||
@@ -523,6 +530,10 @@ async def handle_with_litellm(user_message, model_name, message_history, msg, im
|
||||
|
||||
if 'content' in delta and delta['content'] is not None:
|
||||
token = delta['content']
|
||||
# Send message on first token
|
||||
if not msg_sent:
|
||||
await msg.send()
|
||||
msg_sent = True
|
||||
await msg.stream_token(token)
|
||||
full_response += token
|
||||
|
||||
@@ -549,6 +560,10 @@ async def handle_with_litellm(user_message, model_name, message_history, msg, im
|
||||
if current_tool_call:
|
||||
tool_calls.append(current_tool_call)
|
||||
|
||||
# Ensure message is sent even if no content (tool calls only)
|
||||
if not msg_sent:
|
||||
await msg.send()
|
||||
|
||||
logger.debug(f"Full response: {full_response}")
|
||||
logger.debug(f"Tool calls: {tool_calls}")
|
||||
message_history.append({"role": "assistant", "content": full_response})
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.8.4"
|
||||
__version__ = "2.8.9"
|
||||
@@ -26,7 +26,7 @@ praisonai-call = "praisonai.api.call:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
ui = [
|
||||
"chainlit>=2.9.4,<3.0.0",
|
||||
"chainlit>=2.8.5,<2.9.0",
|
||||
"sqlalchemy>=2.0.36",
|
||||
"aiosqlite>=0.20.0",
|
||||
"greenlet>=3.0.3",
|
||||
@@ -45,7 +45,7 @@ openai = ["langchain-openai>=0.2.1,<0.3.0"]
|
||||
anthropic = ["langchain-anthropic>=0.3.0"]
|
||||
cohere = ["langchain-cohere>=0.3.0,<0.4.0"]
|
||||
chat = [
|
||||
"chainlit>=2.9.4,<3.0.0",
|
||||
"chainlit>=2.8.5,<2.9.0",
|
||||
"litellm>=1.72.6",
|
||||
"aiosqlite>=0.20.0",
|
||||
"greenlet>=3.0.3",
|
||||
@@ -57,7 +57,7 @@ chat = [
|
||||
"pydantic<=2.10.1"
|
||||
]
|
||||
code = [
|
||||
"chainlit>=2.9.4,<3.0.0",
|
||||
"chainlit>=2.8.5,<2.9.0",
|
||||
"litellm>=1.72.6",
|
||||
"aiosqlite>=0.20.0",
|
||||
"greenlet>=3.0.3",
|
||||
@@ -68,7 +68,7 @@ code = [
|
||||
"pydantic<=2.10.1"
|
||||
]
|
||||
realtime = [
|
||||
"chainlit>=2.9.4,<3.0.0",
|
||||
"chainlit>=2.8.5,<2.9.0",
|
||||
"litellm>=1.72.6",
|
||||
"aiosqlite>=0.20.0",
|
||||
"greenlet>=3.0.3",
|
||||
@@ -100,6 +100,9 @@ autogen-v4 = [
|
||||
"praisonai-tools>=0.1.0",
|
||||
"crewai"
|
||||
]
|
||||
acp = [
|
||||
"agent-client-protocol>=0.7.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://docs.praison.ai"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Integration tests for ACP module."""
|
||||
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
Real LLM integration tests for ACP.
|
||||
|
||||
These tests use actual API keys to verify end-to-end functionality.
|
||||
API keys should be set via environment variables.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Skip if no API key
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.environ.get("OPENAI_API_KEY"),
|
||||
reason="OPENAI_API_KEY not set"
|
||||
)
|
||||
|
||||
|
||||
class TestACPRealLLM:
|
||||
"""Real LLM tests for ACP server."""
|
||||
|
||||
@pytest.fixture
|
||||
def acp_server(self):
|
||||
"""Create ACP server with real agent."""
|
||||
from praisonai.acp.server import ACPServer
|
||||
from praisonai.acp.config import ACPConfig
|
||||
|
||||
config = ACPConfig(
|
||||
workspace=Path(tempfile.mkdtemp()),
|
||||
debug=True,
|
||||
model="gpt-4o-mini",
|
||||
)
|
||||
return ACPServer(config=config)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_real(self, acp_server):
|
||||
"""Test real initialize handshake."""
|
||||
result = await acp_server.initialize(
|
||||
protocol_version=1,
|
||||
client_capabilities={"fs": {"readTextFile": True}},
|
||||
client_info={"name": "test-harness", "version": "1.0"},
|
||||
)
|
||||
|
||||
assert result["protocolVersion"] == 1
|
||||
assert result["agentInfo"]["name"] == "praisonai"
|
||||
assert "agentCapabilities" in result
|
||||
|
||||
# Log for verification (masked)
|
||||
print(f"[TEST] Initialize successful: protocol={result['protocolVersion']}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_lifecycle(self, acp_server):
|
||||
"""Test complete session lifecycle."""
|
||||
# Initialize
|
||||
await acp_server.initialize(protocol_version=1)
|
||||
|
||||
# Create session
|
||||
session_result = await acp_server.new_session(
|
||||
cwd="/tmp/test",
|
||||
mcp_servers=[],
|
||||
)
|
||||
|
||||
session_id = session_result["sessionId"]
|
||||
assert session_id.startswith("sess_")
|
||||
|
||||
# Log for verification
|
||||
print(f"[TEST] Session created: {session_id[:20]}...")
|
||||
|
||||
# Set mode
|
||||
await acp_server.set_session_mode(
|
||||
mode_id="manual",
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
# Cancel
|
||||
await acp_server.cancel(session_id=session_id)
|
||||
|
||||
print("[TEST] Session lifecycle complete")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_with_real_llm(self, acp_server):
|
||||
"""Test prompt with real LLM call."""
|
||||
# Initialize
|
||||
await acp_server.initialize(protocol_version=1)
|
||||
|
||||
# Create session
|
||||
session_result = await acp_server.new_session(
|
||||
cwd="/tmp/test",
|
||||
mcp_servers=[],
|
||||
)
|
||||
session_id = session_result["sessionId"]
|
||||
|
||||
# Send a simple prompt
|
||||
prompt_result = await acp_server.prompt(
|
||||
prompt=[{"type": "text", "text": "What is 2+2? Reply with just the number."}],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
# Verify response
|
||||
assert "stopReason" in prompt_result
|
||||
assert prompt_result["stopReason"] in ["end_turn", "cancelled", "refusal"]
|
||||
|
||||
# Log for verification (no secrets)
|
||||
print(f"[TEST] Prompt completed: stopReason={prompt_result['stopReason']}")
|
||||
print(f"[TEST] Session: {session_id[:20]}...")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_resume(self, acp_server):
|
||||
"""Test session resume functionality."""
|
||||
# Initialize
|
||||
await acp_server.initialize(protocol_version=1)
|
||||
|
||||
# Create session
|
||||
session_result = await acp_server.new_session(
|
||||
cwd="/tmp/test",
|
||||
mcp_servers=[],
|
||||
)
|
||||
session_id = session_result["sessionId"]
|
||||
|
||||
# Add a message to the session
|
||||
acp_server._sessions[session_id].add_message("user", "Test message")
|
||||
acp_server._session_store.save(acp_server._sessions[session_id])
|
||||
|
||||
# Create new server instance (simulating restart)
|
||||
from praisonai.acp.config import ACPConfig
|
||||
config = ACPConfig(workspace=Path("/tmp/test"), debug=True)
|
||||
from praisonai.acp.server import ACPServer as ACPServerClass
|
||||
new_server = ACPServerClass(config=config)
|
||||
|
||||
# Resume session
|
||||
resume_result = await new_server.load_session(
|
||||
cwd="/tmp/test",
|
||||
mcp_servers=[],
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
assert resume_result is not None
|
||||
assert session_id in new_server._sessions
|
||||
|
||||
# Verify message was preserved
|
||||
loaded_session = new_server._sessions[session_id]
|
||||
assert len(loaded_session.messages) == 1
|
||||
assert loaded_session.messages[0]["content"] == "Test message"
|
||||
|
||||
print(f"[TEST] Session resume successful: {session_id[:20]}...")
|
||||
|
||||
|
||||
class TestACPPermissionGating:
|
||||
"""Tests for permission gating."""
|
||||
|
||||
def test_read_only_default(self):
|
||||
"""Verify read-only is default."""
|
||||
from praisonai.acp.config import ACPConfig
|
||||
|
||||
config = ACPConfig()
|
||||
assert config.read_only is True
|
||||
assert config.can_write() is False
|
||||
assert config.can_execute_shell() is False
|
||||
|
||||
def test_explicit_write_permission(self):
|
||||
"""Test explicit write permission."""
|
||||
from praisonai.acp.config import ACPConfig
|
||||
|
||||
config = ACPConfig(allow_write=True)
|
||||
assert config.can_write() is True
|
||||
|
||||
def test_workspace_boundary(self):
|
||||
"""Test workspace boundary enforcement."""
|
||||
from praisonai.acp.config import ACPConfig
|
||||
|
||||
config = ACPConfig(workspace="/tmp/test_workspace")
|
||||
|
||||
# Within workspace - allowed
|
||||
assert config.is_path_allowed(Path("/tmp/test_workspace/file.txt"))
|
||||
assert config.is_path_allowed(Path("/tmp/test_workspace/subdir/file.txt"))
|
||||
|
||||
# Outside workspace - not allowed
|
||||
assert not config.is_path_allowed(Path("/etc/passwd"))
|
||||
assert not config.is_path_allowed(Path("/var/log/system.log"))
|
||||
|
||||
|
||||
class TestACPTracing:
|
||||
"""Tests for tracing and attribution."""
|
||||
|
||||
def test_session_has_trace_ids(self):
|
||||
"""Verify sessions have trace IDs."""
|
||||
from praisonai.acp.session import ACPSession
|
||||
|
||||
session = ACPSession.create(
|
||||
workspace=Path("/tmp/test"),
|
||||
agent_id="test_agent",
|
||||
)
|
||||
|
||||
assert session.session_id is not None
|
||||
assert session.run_id is not None
|
||||
assert session.trace_id is not None
|
||||
assert session.agent_id == "test_agent"
|
||||
|
||||
# IDs should have proper prefixes
|
||||
assert session.session_id.startswith("sess_")
|
||||
assert session.run_id.startswith("run_")
|
||||
assert session.trace_id.startswith("trace_")
|
||||
|
||||
print(f"[TEST] Trace IDs: session={session.session_id[:15]}, run={session.run_id}, trace={session.trace_id}")
|
||||
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
Integration tests for ACP stdio communication.
|
||||
|
||||
Tests the ACP server by spawning it as a subprocess and communicating
|
||||
via JSON-RPC over stdio.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestACPStdioIntegration:
|
||||
"""Integration tests for ACP stdio communication."""
|
||||
|
||||
@pytest.fixture
|
||||
def acp_process(self):
|
||||
"""Start ACP server as subprocess."""
|
||||
# Use the CLI module directly to avoid crewai import issues
|
||||
proc = subprocess.Popen(
|
||||
[
|
||||
sys.executable, "-c",
|
||||
"""
|
||||
import sys
|
||||
import asyncio
|
||||
from praisonai.acp.server import ACPServer
|
||||
from praisonai.acp.config import ACPConfig
|
||||
|
||||
async def run():
|
||||
config = ACPConfig(debug=True)
|
||||
server = ACPServer(config=config)
|
||||
|
||||
# Simple JSON-RPC handler for testing
|
||||
import json
|
||||
|
||||
while True:
|
||||
try:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
request = json.loads(line.strip())
|
||||
method = request.get("method", "")
|
||||
params = request.get("params", {})
|
||||
req_id = request.get("id")
|
||||
|
||||
if method == "initialize":
|
||||
result = await server.initialize(
|
||||
protocol_version=params.get("protocolVersion", 1),
|
||||
client_capabilities=params.get("clientCapabilities"),
|
||||
client_info=params.get("clientInfo"),
|
||||
)
|
||||
elif method == "session/new":
|
||||
result = await server.new_session(
|
||||
cwd=params.get("cwd", "."),
|
||||
mcp_servers=params.get("mcpServers", []),
|
||||
)
|
||||
elif method == "session/cancel":
|
||||
await server.cancel(session_id=params.get("sessionId", ""))
|
||||
result = None
|
||||
else:
|
||||
result = {"error": f"Unknown method: {method}"}
|
||||
|
||||
response = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"result": result,
|
||||
}
|
||||
print(json.dumps(response), flush=True)
|
||||
except Exception as e:
|
||||
print(json.dumps({"jsonrpc": "2.0", "id": None, "error": {"code": -32603, "message": str(e)}}), flush=True)
|
||||
|
||||
asyncio.run(run())
|
||||
"""
|
||||
],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
yield proc
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
|
||||
def send_request(self, proc, method: str, params: dict, req_id: int = 1) -> dict:
|
||||
"""Send JSON-RPC request and get response."""
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": req_id,
|
||||
"method": method,
|
||||
"params": params,
|
||||
}
|
||||
proc.stdin.write(json.dumps(request) + "\n")
|
||||
proc.stdin.flush()
|
||||
|
||||
# Read response with timeout
|
||||
response_line = proc.stdout.readline()
|
||||
if response_line:
|
||||
return json.loads(response_line.strip())
|
||||
return None
|
||||
|
||||
def test_initialize(self, acp_process):
|
||||
"""Test initialize handshake."""
|
||||
response = self.send_request(
|
||||
acp_process,
|
||||
"initialize",
|
||||
{
|
||||
"protocolVersion": 1,
|
||||
"clientCapabilities": {"fs": {"readTextFile": True}},
|
||||
"clientInfo": {"name": "test-client", "version": "1.0"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response["jsonrpc"] == "2.0"
|
||||
assert response["id"] == 1
|
||||
assert "result" in response
|
||||
|
||||
result = response["result"]
|
||||
assert result["protocolVersion"] == 1
|
||||
assert "agentCapabilities" in result
|
||||
assert result["agentInfo"]["name"] == "praisonai"
|
||||
|
||||
def test_new_session(self, acp_process):
|
||||
"""Test session creation."""
|
||||
# First initialize
|
||||
self.send_request(
|
||||
acp_process,
|
||||
"initialize",
|
||||
{"protocolVersion": 1},
|
||||
req_id=1,
|
||||
)
|
||||
|
||||
# Then create session
|
||||
response = self.send_request(
|
||||
acp_process,
|
||||
"session/new",
|
||||
{
|
||||
"cwd": "/tmp/test",
|
||||
"mcpServers": [],
|
||||
},
|
||||
req_id=2,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response["id"] == 2
|
||||
assert "result" in response
|
||||
|
||||
result = response["result"]
|
||||
assert "sessionId" in result
|
||||
assert result["sessionId"].startswith("sess_")
|
||||
|
||||
def test_cancel_notification(self, acp_process):
|
||||
"""Test cancel notification."""
|
||||
# Initialize
|
||||
self.send_request(acp_process, "initialize", {"protocolVersion": 1}, req_id=1)
|
||||
|
||||
# Create session
|
||||
new_resp = self.send_request(
|
||||
acp_process,
|
||||
"session/new",
|
||||
{"cwd": "/tmp", "mcpServers": []},
|
||||
req_id=2,
|
||||
)
|
||||
session_id = new_resp["result"]["sessionId"]
|
||||
|
||||
# Send cancel (notification, but we're treating it as request for testing)
|
||||
response = self.send_request(
|
||||
acp_process,
|
||||
"session/cancel",
|
||||
{"sessionId": session_id},
|
||||
req_id=3,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert response["id"] == 3
|
||||
|
||||
|
||||
class TestACPNoStdoutPollution:
|
||||
"""Tests to ensure ACP doesn't pollute stdout."""
|
||||
|
||||
def test_no_print_statements(self):
|
||||
"""Verify ACP modules don't have print statements that would pollute stdout."""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
acp_dir = Path(__file__).parent.parent.parent.parent / "praisonai" / "acp"
|
||||
|
||||
for py_file in acp_dir.glob("*.py"):
|
||||
content = py_file.read_text()
|
||||
try:
|
||||
tree = ast.parse(content)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Call):
|
||||
if isinstance(node.func, ast.Name) and node.func.id == "print":
|
||||
# Check if it's inside a function that's not for debugging
|
||||
pytest.fail(f"Found print() in {py_file.name} - stdout must be reserved for JSON-RPC")
|
||||
except SyntaxError:
|
||||
pass # Skip files with syntax errors
|
||||
|
||||
def test_logging_to_stderr(self):
|
||||
"""Verify logging is configured for stderr."""
|
||||
from praisonai.acp.server import _setup_stderr_logging
|
||||
import logging
|
||||
import sys
|
||||
|
||||
_setup_stderr_logging(debug=True)
|
||||
|
||||
logger = logging.getLogger("praisonai.acp")
|
||||
|
||||
# Check that handlers write to stderr
|
||||
for handler in logger.handlers:
|
||||
if hasattr(handler, 'stream'):
|
||||
assert handler.stream == sys.stderr, "Logger must write to stderr, not stdout"
|
||||
|
||||
|
||||
class TestACPInteractiveModeCompatibility:
|
||||
"""Tests for ACP and interactive mode compatibility."""
|
||||
|
||||
def test_acp_import_does_not_break_interactive(self):
|
||||
"""Verify importing ACP doesn't break interactive mode imports."""
|
||||
# This should not raise any errors
|
||||
from praisonai.acp import ACPConfig
|
||||
|
||||
# Interactive mode imports should still work
|
||||
from praisonai.cli.features.interactive_tui import InteractiveConfig
|
||||
|
||||
# Both should be usable
|
||||
acp_config = ACPConfig()
|
||||
interactive_config = InteractiveConfig()
|
||||
|
||||
assert acp_config is not None
|
||||
assert interactive_config is not None
|
||||
|
||||
def test_acp_lazy_import(self):
|
||||
"""Verify ACP uses lazy imports."""
|
||||
import time
|
||||
|
||||
start = time.time()
|
||||
import praisonai.acp as acp_module # noqa: F401
|
||||
import_time = time.time() - start
|
||||
|
||||
# Import should be fast (< 100ms)
|
||||
assert import_time < 0.1, f"ACP import too slow: {import_time}s"
|
||||
|
||||
def test_interactive_mode_without_acp_sdk(self):
|
||||
"""Verify interactive mode works even if ACP SDK is not installed."""
|
||||
# Mock the ACP SDK not being available
|
||||
import sys
|
||||
|
||||
# Save original
|
||||
original_modules = sys.modules.copy()
|
||||
|
||||
try:
|
||||
# Remove acp from modules if present
|
||||
if 'acp' in sys.modules:
|
||||
del sys.modules['acp']
|
||||
|
||||
# Interactive mode should still work
|
||||
from praisonai.cli.features.interactive_tui import InteractiveSession
|
||||
|
||||
session = InteractiveSession()
|
||||
assert session is not None
|
||||
finally:
|
||||
# Restore
|
||||
sys.modules.update(original_modules)
|
||||
|
||||
|
||||
class TestACPSessionPersistence:
|
||||
"""Tests for ACP session persistence."""
|
||||
|
||||
def test_session_save_and_load(self):
|
||||
"""Test session persistence across server restarts."""
|
||||
from praisonai.acp.session import ACPSession, SessionStore
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
# Create and save session
|
||||
session = ACPSession.create(workspace=Path("/tmp/test"))
|
||||
session.add_message("user", "Hello")
|
||||
session.add_message("assistant", "Hi there!")
|
||||
store.save(session)
|
||||
|
||||
# Create new store instance (simulating restart)
|
||||
store2 = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
# Load session
|
||||
loaded = store2.load(session.session_id)
|
||||
|
||||
assert loaded is not None
|
||||
assert loaded.session_id == session.session_id
|
||||
assert len(loaded.messages) == 2
|
||||
assert loaded.messages[0]["content"] == "Hello"
|
||||
|
||||
def test_resume_last_session(self):
|
||||
"""Test --resume --last functionality."""
|
||||
from praisonai.acp.session import ACPSession, SessionStore
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
# Create multiple sessions
|
||||
session1 = ACPSession.create(workspace=Path("/tmp/test1"))
|
||||
store.save(session1)
|
||||
|
||||
time.sleep(0.01) # Ensure different timestamps
|
||||
|
||||
session2 = ACPSession.create(workspace=Path("/tmp/test2"))
|
||||
session2.add_message("user", "Last session message")
|
||||
store.save(session2)
|
||||
|
||||
# Load last session
|
||||
last = store.load_last()
|
||||
|
||||
assert last is not None
|
||||
assert last.session_id == session2.session_id
|
||||
assert len(last.messages) == 1
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests for ACP module."""
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Unit tests for ACP configuration."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from praisonai.acp.config import ACPConfig
|
||||
|
||||
|
||||
class TestACPConfig:
|
||||
"""Tests for ACPConfig class."""
|
||||
|
||||
def test_default_config(self):
|
||||
"""Test default configuration values."""
|
||||
config = ACPConfig()
|
||||
|
||||
assert config.workspace == Path.cwd()
|
||||
assert config.agent == "default"
|
||||
assert config.agents_config is None
|
||||
assert config.router_enabled is False
|
||||
assert config.model is None
|
||||
assert config.resume_session is None
|
||||
assert config.resume_last is False
|
||||
assert config.read_only is True
|
||||
assert config.allow_write is False
|
||||
assert config.allow_shell is False
|
||||
assert config.allow_network is False
|
||||
assert config.approval_mode == "manual"
|
||||
assert config.debug is False
|
||||
assert config.profile is None
|
||||
|
||||
def test_workspace_normalization(self):
|
||||
"""Test workspace path normalization."""
|
||||
config = ACPConfig(workspace=".")
|
||||
assert config.workspace == Path.cwd()
|
||||
|
||||
config = ACPConfig(workspace="/tmp")
|
||||
# Use resolve() to handle macOS symlinks (/tmp -> /private/tmp)
|
||||
assert config.workspace == Path("/tmp").resolve()
|
||||
|
||||
def test_allowed_paths_default(self):
|
||||
"""Test default allowed paths."""
|
||||
config = ACPConfig()
|
||||
|
||||
assert config.workspace in config.allowed_paths
|
||||
assert Path.home() / ".praison" in config.allowed_paths
|
||||
|
||||
def test_is_path_allowed(self):
|
||||
"""Test path allowlist checking."""
|
||||
config = ACPConfig(workspace="/tmp/test_workspace")
|
||||
|
||||
# Workspace itself should be allowed
|
||||
assert config.is_path_allowed(Path("/tmp/test_workspace"))
|
||||
|
||||
# Subdirectories should be allowed
|
||||
assert config.is_path_allowed(Path("/tmp/test_workspace/subdir"))
|
||||
|
||||
# Parent directories should NOT be allowed
|
||||
assert not config.is_path_allowed(Path("/tmp"))
|
||||
|
||||
# Unrelated paths should NOT be allowed
|
||||
assert not config.is_path_allowed(Path("/var/log"))
|
||||
|
||||
def test_can_write(self):
|
||||
"""Test write permission checking."""
|
||||
# Read-only mode
|
||||
config = ACPConfig(read_only=True, allow_write=False)
|
||||
assert not config.can_write()
|
||||
|
||||
# Allow write overrides read-only
|
||||
config = ACPConfig(read_only=True, allow_write=True)
|
||||
assert config.can_write()
|
||||
|
||||
# Not read-only
|
||||
config = ACPConfig(read_only=False)
|
||||
assert config.can_write()
|
||||
|
||||
def test_can_execute_shell(self):
|
||||
"""Test shell execution permission checking."""
|
||||
config = ACPConfig(allow_shell=False)
|
||||
assert not config.can_execute_shell()
|
||||
|
||||
config = ACPConfig(allow_shell=True)
|
||||
assert config.can_execute_shell()
|
||||
|
||||
def test_from_env(self, monkeypatch):
|
||||
"""Test configuration from environment variables."""
|
||||
monkeypatch.setenv("PRAISONAI_WORKSPACE", "/tmp/env_workspace")
|
||||
monkeypatch.setenv("PRAISONAI_AGENT", "test_agent")
|
||||
monkeypatch.setenv("PRAISONAI_MODEL", "gpt-4")
|
||||
monkeypatch.setenv("PRAISONAI_DEBUG", "true")
|
||||
monkeypatch.setenv("PRAISONAI_READ_ONLY", "false")
|
||||
monkeypatch.setenv("PRAISONAI_APPROVAL_MODE", "auto")
|
||||
|
||||
config = ACPConfig.from_env()
|
||||
|
||||
# Use resolve() to handle macOS symlinks
|
||||
assert config.workspace == Path("/tmp/env_workspace").resolve()
|
||||
assert config.agent == "test_agent"
|
||||
assert config.model == "gpt-4"
|
||||
assert config.debug is True
|
||||
assert config.read_only is False
|
||||
assert config.approval_mode == "auto"
|
||||
|
||||
def test_from_file(self):
|
||||
"""Test configuration from YAML file."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
||||
f.write("""
|
||||
acp:
|
||||
workspace: /tmp/file_workspace
|
||||
agent: file_agent
|
||||
model: gpt-3.5-turbo
|
||||
debug: true
|
||||
read_only: false
|
||||
approval_mode: scoped
|
||||
""")
|
||||
f.flush()
|
||||
|
||||
config = ACPConfig.from_file(Path(f.name))
|
||||
|
||||
# Use resolve() to handle macOS symlinks
|
||||
assert config.workspace == Path("/tmp/file_workspace").resolve()
|
||||
assert config.agent == "file_agent"
|
||||
assert config.model == "gpt-3.5-turbo"
|
||||
assert config.debug is True
|
||||
assert config.read_only is False
|
||||
assert config.approval_mode == "scoped"
|
||||
|
||||
os.unlink(f.name)
|
||||
|
||||
def test_from_file_missing(self):
|
||||
"""Test configuration from non-existent file."""
|
||||
config = ACPConfig.from_file(Path("/nonexistent/config.yaml"))
|
||||
|
||||
# Should return default config
|
||||
assert config.agent == "default"
|
||||
assert config.debug is False
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test configuration serialization."""
|
||||
config = ACPConfig(
|
||||
workspace="/tmp/test",
|
||||
agent="test_agent",
|
||||
model="gpt-4",
|
||||
debug=True,
|
||||
)
|
||||
|
||||
data = config.to_dict()
|
||||
|
||||
# Use resolve() to handle macOS symlinks
|
||||
assert data["workspace"] == str(Path("/tmp/test").resolve())
|
||||
assert data["agent"] == "test_agent"
|
||||
assert data["model"] == "gpt-4"
|
||||
assert data["debug"] is True
|
||||
|
||||
def test_merge(self):
|
||||
"""Test configuration merging."""
|
||||
config1 = ACPConfig(agent="agent1", model="model1")
|
||||
config2 = ACPConfig(agent="agent2", debug=True)
|
||||
|
||||
merged = ACPConfig.merge(config1, config2)
|
||||
|
||||
# Later config overrides
|
||||
assert merged.agent == "agent2"
|
||||
assert merged.model == "model1" # Not overridden
|
||||
assert merged.debug is True
|
||||
@@ -0,0 +1,244 @@
|
||||
"""Unit tests for ACP server."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from praisonai.acp.config import ACPConfig
|
||||
from praisonai.acp.server import ACPServer
|
||||
|
||||
|
||||
class TestACPServer:
|
||||
"""Tests for ACPServer class."""
|
||||
|
||||
def test_server_init(self):
|
||||
"""Test server initialization."""
|
||||
config = ACPConfig(workspace="/tmp/test", debug=True)
|
||||
server = ACPServer(config=config)
|
||||
|
||||
assert server.config == config
|
||||
assert server._sessions == {}
|
||||
assert server._cancelled_sessions == set()
|
||||
|
||||
def test_server_init_default_config(self):
|
||||
"""Test server initialization with default config."""
|
||||
server = ACPServer()
|
||||
|
||||
assert server.config is not None
|
||||
assert server.config.workspace == Path.cwd()
|
||||
|
||||
def test_initialize(self):
|
||||
"""Test initialize handler."""
|
||||
server = ACPServer()
|
||||
|
||||
result = asyncio.run(server.initialize(
|
||||
protocol_version=1,
|
||||
client_capabilities={"fs": {"readTextFile": True}},
|
||||
client_info={"name": "test-client", "version": "1.0"},
|
||||
))
|
||||
|
||||
assert result["protocolVersion"] == 1
|
||||
assert "agentCapabilities" in result
|
||||
assert result["agentCapabilities"]["loadSession"] is True
|
||||
assert result["agentInfo"]["name"] == "praisonai"
|
||||
|
||||
def test_new_session(self):
|
||||
"""Test new_session handler."""
|
||||
server = ACPServer()
|
||||
|
||||
result = asyncio.run(server.new_session(
|
||||
cwd="/tmp/test_workspace",
|
||||
mcp_servers=[],
|
||||
))
|
||||
|
||||
assert "sessionId" in result
|
||||
assert result["sessionId"].startswith("sess_")
|
||||
assert result["sessionId"] in server._sessions
|
||||
|
||||
def test_new_session_with_mcp_servers(self):
|
||||
"""Test new_session with MCP servers."""
|
||||
server = ACPServer()
|
||||
|
||||
mcp_servers = [
|
||||
{"name": "test", "command": "/usr/bin/test", "args": []}
|
||||
]
|
||||
|
||||
result = asyncio.run(server.new_session(
|
||||
cwd="/tmp/test",
|
||||
mcp_servers=mcp_servers,
|
||||
))
|
||||
|
||||
session = server._sessions[result["sessionId"]]
|
||||
assert session.mcp_servers == mcp_servers
|
||||
|
||||
def test_set_session_mode(self):
|
||||
"""Test set_session_mode handler."""
|
||||
server = ACPServer()
|
||||
|
||||
# Create a session first
|
||||
new_result = asyncio.run(server.new_session(
|
||||
cwd="/tmp/test",
|
||||
mcp_servers=[],
|
||||
))
|
||||
session_id = new_result["sessionId"]
|
||||
|
||||
# Set mode
|
||||
result = asyncio.run(server.set_session_mode(
|
||||
mode_id="auto",
|
||||
session_id=session_id,
|
||||
))
|
||||
|
||||
assert result == {}
|
||||
assert server._sessions[session_id].mode == "auto"
|
||||
|
||||
def test_cancel(self):
|
||||
"""Test cancel handler."""
|
||||
server = ACPServer()
|
||||
|
||||
# Create a session first
|
||||
new_result = asyncio.run(server.new_session(
|
||||
cwd="/tmp/test",
|
||||
mcp_servers=[],
|
||||
))
|
||||
session_id = new_result["sessionId"]
|
||||
|
||||
# Cancel it
|
||||
asyncio.run(server.cancel(session_id=session_id))
|
||||
|
||||
assert session_id in server._cancelled_sessions
|
||||
|
||||
def test_authenticate(self):
|
||||
"""Test authenticate handler."""
|
||||
server = ACPServer()
|
||||
|
||||
result = asyncio.run(server.authenticate(method_id="test"))
|
||||
|
||||
assert result == {}
|
||||
|
||||
def test_list_sessions(self):
|
||||
"""Test list_sessions handler."""
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
from praisonai.acp.session import SessionStore
|
||||
server = ACPServer()
|
||||
# Use isolated session store
|
||||
server._session_store = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
# Create some sessions
|
||||
asyncio.run(server.new_session(cwd="/tmp/test1", mcp_servers=[]))
|
||||
asyncio.run(server.new_session(cwd="/tmp/test2", mcp_servers=[]))
|
||||
|
||||
result = asyncio.run(server.list_sessions())
|
||||
|
||||
assert "sessions" in result
|
||||
assert len(result["sessions"]) == 2
|
||||
|
||||
def test_fork_session(self):
|
||||
"""Test fork_session handler."""
|
||||
server = ACPServer()
|
||||
|
||||
# Create original session
|
||||
original = asyncio.run(server.new_session(
|
||||
cwd="/tmp/test",
|
||||
mcp_servers=[],
|
||||
))
|
||||
original_id = original["sessionId"]
|
||||
|
||||
# Add a message to original
|
||||
server._sessions[original_id].add_message("user", "Hello")
|
||||
|
||||
# Fork it
|
||||
forked = asyncio.run(server.fork_session(
|
||||
cwd="/tmp/test2",
|
||||
session_id=original_id,
|
||||
))
|
||||
|
||||
assert forked["sessionId"] != original_id
|
||||
assert forked["sessionId"] in server._sessions
|
||||
|
||||
# Forked session should have the message
|
||||
forked_session = server._sessions[forked["sessionId"]]
|
||||
assert len(forked_session.messages) == 1
|
||||
|
||||
def test_extract_prompt_text(self):
|
||||
"""Test prompt text extraction."""
|
||||
server = ACPServer()
|
||||
|
||||
prompt = [
|
||||
{"type": "text", "text": "Hello world"},
|
||||
{"type": "text", "text": "How are you?"},
|
||||
]
|
||||
|
||||
result = server._extract_prompt_text(prompt)
|
||||
|
||||
assert "Hello world" in result
|
||||
assert "How are you?" in result
|
||||
|
||||
def test_extract_prompt_text_with_resource(self):
|
||||
"""Test prompt text extraction with resource."""
|
||||
server = ACPServer()
|
||||
|
||||
prompt = [
|
||||
{"type": "text", "text": "Check this file:"},
|
||||
{
|
||||
"type": "resource",
|
||||
"resource": {
|
||||
"uri": "file:///tmp/test.py",
|
||||
"text": "print('hello')",
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
result = server._extract_prompt_text(prompt)
|
||||
|
||||
assert "Check this file:" in result
|
||||
assert "print('hello')" in result
|
||||
assert "file:///tmp/test.py" in result
|
||||
|
||||
def test_get_available_modes(self):
|
||||
"""Test available modes."""
|
||||
server = ACPServer()
|
||||
|
||||
modes = server._get_available_modes()
|
||||
|
||||
assert modes is not None
|
||||
assert len(modes) >= 2
|
||||
|
||||
mode_ids = [m["id"] for m in modes]
|
||||
assert "manual" in mode_ids
|
||||
assert "auto" in mode_ids
|
||||
|
||||
|
||||
class TestACPServerPrompt:
|
||||
"""Tests for ACPServer prompt handling."""
|
||||
|
||||
def test_prompt_cancelled(self):
|
||||
"""Test prompt with cancelled session."""
|
||||
server = ACPServer()
|
||||
|
||||
# Create session
|
||||
new_result = asyncio.run(server.new_session(
|
||||
cwd="/tmp/test",
|
||||
mcp_servers=[],
|
||||
))
|
||||
session_id = new_result["sessionId"]
|
||||
|
||||
# Mark as cancelled
|
||||
server._cancelled_sessions.add(session_id)
|
||||
|
||||
# Try to prompt
|
||||
result = asyncio.run(server.prompt(
|
||||
prompt=[{"type": "text", "text": "Hello"}],
|
||||
session_id=session_id,
|
||||
))
|
||||
|
||||
assert result["stopReason"] == "cancelled"
|
||||
|
||||
def test_prompt_nonexistent_session(self):
|
||||
"""Test prompt with non-existent session."""
|
||||
server = ACPServer()
|
||||
|
||||
result = asyncio.run(server.prompt(
|
||||
prompt=[{"type": "text", "text": "Hello"}],
|
||||
session_id="nonexistent",
|
||||
))
|
||||
|
||||
assert result["stopReason"] == "refusal"
|
||||
@@ -0,0 +1,203 @@
|
||||
"""Unit tests for ACP session management."""
|
||||
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from praisonai.acp.session import ACPSession, SessionStore
|
||||
|
||||
|
||||
class TestACPSession:
|
||||
"""Tests for ACPSession class."""
|
||||
|
||||
def test_create_session(self):
|
||||
"""Test session creation."""
|
||||
session = ACPSession.create(
|
||||
workspace=Path("/tmp/test"),
|
||||
agent_id="test_agent",
|
||||
)
|
||||
|
||||
assert session.session_id.startswith("sess_")
|
||||
assert session.workspace == Path("/tmp/test")
|
||||
assert session.agent_id == "test_agent"
|
||||
assert session.run_id is not None
|
||||
assert session.trace_id is not None
|
||||
assert session.mode == "manual"
|
||||
|
||||
def test_update_activity(self):
|
||||
"""Test activity timestamp update."""
|
||||
session = ACPSession.create(workspace=Path("/tmp"))
|
||||
original_time = session.last_activity
|
||||
|
||||
time.sleep(0.01)
|
||||
session.update_activity()
|
||||
|
||||
assert session.last_activity > original_time
|
||||
|
||||
def test_add_message(self):
|
||||
"""Test adding messages to session."""
|
||||
session = ACPSession.create(workspace=Path("/tmp"))
|
||||
|
||||
session.add_message("user", "Hello")
|
||||
session.add_message("assistant", "Hi there!")
|
||||
|
||||
assert len(session.messages) == 2
|
||||
assert session.messages[0]["role"] == "user"
|
||||
assert session.messages[0]["content"] == "Hello"
|
||||
assert session.messages[1]["role"] == "assistant"
|
||||
assert session.messages[1]["content"] == "Hi there!"
|
||||
|
||||
def test_add_tool_call(self):
|
||||
"""Test adding tool calls to session."""
|
||||
session = ACPSession.create(workspace=Path("/tmp"))
|
||||
|
||||
session.add_tool_call(
|
||||
tool_call_id="call_001",
|
||||
title="Read file",
|
||||
status="completed",
|
||||
path="/tmp/test.txt",
|
||||
)
|
||||
|
||||
assert len(session.tool_calls) == 1
|
||||
assert session.tool_calls[0]["tool_call_id"] == "call_001"
|
||||
assert session.tool_calls[0]["title"] == "Read file"
|
||||
assert session.tool_calls[0]["status"] == "completed"
|
||||
assert session.tool_calls[0]["path"] == "/tmp/test.txt"
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test session serialization."""
|
||||
session = ACPSession.create(
|
||||
workspace=Path("/tmp/test"),
|
||||
agent_id="test_agent",
|
||||
)
|
||||
session.add_message("user", "Test message")
|
||||
|
||||
data = session.to_dict()
|
||||
|
||||
assert data["session_id"] == session.session_id
|
||||
assert data["workspace"] == "/tmp/test"
|
||||
assert data["agent_id"] == "test_agent"
|
||||
assert len(data["messages"]) == 1
|
||||
|
||||
def test_from_dict(self):
|
||||
"""Test session deserialization."""
|
||||
data = {
|
||||
"session_id": "sess_test123",
|
||||
"workspace": "/tmp/test",
|
||||
"created_at": 1234567890.0,
|
||||
"last_activity": 1234567891.0,
|
||||
"agent_id": "test_agent",
|
||||
"run_id": "run_abc",
|
||||
"trace_id": "trace_xyz",
|
||||
"mode": "auto",
|
||||
"model": "gpt-4",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"tool_calls": [],
|
||||
"mcp_servers": [],
|
||||
}
|
||||
|
||||
session = ACPSession.from_dict(data)
|
||||
|
||||
assert session.session_id == "sess_test123"
|
||||
assert session.workspace == Path("/tmp/test")
|
||||
assert session.agent_id == "test_agent"
|
||||
assert session.mode == "auto"
|
||||
assert session.model == "gpt-4"
|
||||
assert len(session.messages) == 1
|
||||
|
||||
|
||||
class TestSessionStore:
|
||||
"""Tests for SessionStore class."""
|
||||
|
||||
def test_save_and_load(self):
|
||||
"""Test saving and loading sessions."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
session = ACPSession.create(workspace=Path("/tmp/test"))
|
||||
session.add_message("user", "Test")
|
||||
|
||||
store.save(session)
|
||||
|
||||
loaded = store.load(session.session_id)
|
||||
|
||||
assert loaded is not None
|
||||
assert loaded.session_id == session.session_id
|
||||
assert len(loaded.messages) == 1
|
||||
|
||||
def test_load_nonexistent(self):
|
||||
"""Test loading non-existent session."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
loaded = store.load("nonexistent_session")
|
||||
|
||||
assert loaded is None
|
||||
|
||||
def test_load_last(self):
|
||||
"""Test loading the last session."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
# Create and save multiple sessions
|
||||
session1 = ACPSession.create(workspace=Path("/tmp/test1"))
|
||||
store.save(session1)
|
||||
|
||||
session2 = ACPSession.create(workspace=Path("/tmp/test2"))
|
||||
store.save(session2)
|
||||
|
||||
# Last saved should be session2
|
||||
last = store.load_last()
|
||||
|
||||
assert last is not None
|
||||
assert last.session_id == session2.session_id
|
||||
|
||||
def test_delete(self):
|
||||
"""Test deleting a session."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
session = ACPSession.create(workspace=Path("/tmp/test"))
|
||||
store.save(session)
|
||||
|
||||
# Verify it exists
|
||||
assert store.load(session.session_id) is not None
|
||||
|
||||
# Delete it
|
||||
result = store.delete(session.session_id)
|
||||
assert result is True
|
||||
|
||||
# Verify it's gone
|
||||
assert store.load(session.session_id) is None
|
||||
|
||||
def test_list_sessions(self):
|
||||
"""Test listing sessions."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
# Create multiple sessions
|
||||
for i in range(3):
|
||||
session = ACPSession.create(workspace=Path(f"/tmp/test{i}"))
|
||||
store.save(session)
|
||||
time.sleep(0.01) # Ensure different timestamps
|
||||
|
||||
sessions = store.list_sessions()
|
||||
|
||||
assert len(sessions) == 3
|
||||
# Should be sorted by last_activity, most recent first
|
||||
assert sessions[0].last_activity >= sessions[1].last_activity
|
||||
assert sessions[1].last_activity >= sessions[2].last_activity
|
||||
|
||||
def test_list_sessions_limit(self):
|
||||
"""Test listing sessions with limit."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = SessionStore(storage_dir=Path(tmpdir))
|
||||
|
||||
# Create multiple sessions
|
||||
for i in range(5):
|
||||
session = ACPSession.create(workspace=Path(f"/tmp/test{i}"))
|
||||
store.save(session)
|
||||
|
||||
sessions = store.list_sessions(limit=2)
|
||||
|
||||
assert len(sessions) == 2
|
||||
Generated
+39
-102
@@ -9,6 +9,18 @@ resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/7c/12da39be4f73026fd9b02144df5f64d803488cf1439aa221b0edb7c305e3/agent_client_protocol-0.7.1.tar.gz", hash = "sha256:8d7031209e14c3f2f987e3b95e7d9c3286158e7b2af1bf43d6aae5b8a429249f", size = 66226, upload-time = "2025-12-28T13:58:57.012Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/03/48/48d2fb454f911147432cd779f548e188274e1700f1cbe0a258e78158331a/agent_client_protocol-0.7.1-py3-none-any.whl", hash = "sha256:4ffe999488f2b23db26f09becdfaa2aaae6529f0847a52bca61bc2c628001c0f", size = 53771, upload-time = "2025-12-28T13:58:55.967Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentops"
|
||||
version = "0.3.16"
|
||||
@@ -612,7 +624,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "chainlit"
|
||||
version = "2.9.4"
|
||||
version = "2.8.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
@@ -640,9 +652,9 @@ dependencies = [
|
||||
{ name = "uvicorn" },
|
||||
{ name = "watchfiles" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/2e/24bfd3df49ef3f7549918406e17355e6b08877fd5954a419f6fd2faebc0e/chainlit-2.9.4.tar.gz", hash = "sha256:0b79c5e1f9e7d0e8b776b48ea8a0e11a1b196d71a6b5746a5d873b53817afd2a", size = 10063724, upload-time = "2025-12-25T03:41:25.419Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/71/d0465682910792ff382d604b9973dcd0115c682dde6c22753eaa4304afe3/chainlit-2.8.5.tar.gz", hash = "sha256:ebe73b3ad9d7e545454432eb280618ef92d69d6470eb66488149c9bb7f6c526d", size = 9884512, upload-time = "2025-11-07T22:58:11.181Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/1f/ba9329ae92de00169eb0ddc92cd79d2cc9f663a4ce18d8bcddbe777bc3bb/chainlit-2.9.4-py3-none-any.whl", hash = "sha256:c9bd8cce22bc741003bf100b4141393a8b9964ef7c818c95aa28967bd9aae62e", size = 9808194, upload-time = "2025-12-25T03:41:22.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/e2/c9a1ff354acc05fe910a19a8053647674315c091f33fe3b1eef3c69fcbe7/chainlit-2.8.5-py3-none-any.whl", hash = "sha256:1d6ef81a14f4b9a526229b0e6839f364c8ef1a3335e648889d43c0c8f8444a8e", size = 9725985, upload-time = "2025-11-07T22:58:08.631Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3979,6 +3991,9 @@ dependencies = [
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
acp = [
|
||||
{ name = "agent-client-protocol" },
|
||||
]
|
||||
agentops = [
|
||||
{ name = "agentops" },
|
||||
]
|
||||
@@ -4075,6 +4090,7 @@ ui = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "agent-client-protocol", marker = "extra == 'acp'", specifier = ">=0.7.0" },
|
||||
{ name = "agentops", marker = "extra == 'agentops'", specifier = ">=0.3.12" },
|
||||
{ name = "aiosqlite", marker = "extra == 'chat'", specifier = ">=0.20.0" },
|
||||
{ name = "aiosqlite", marker = "extra == 'code'", specifier = ">=0.20.0" },
|
||||
@@ -4083,10 +4099,10 @@ requires-dist = [
|
||||
{ name = "autogen-agentchat", marker = "extra == 'autogen-v4'", specifier = ">=0.4.0" },
|
||||
{ name = "autogen-core", marker = "extra == 'autogen-v4'", specifier = ">=0.4.0" },
|
||||
{ name = "autogen-ext", extras = ["openai"], marker = "extra == 'autogen-v4'", specifier = ">=0.4.0" },
|
||||
{ name = "chainlit", marker = "extra == 'chat'", specifier = ">=2.9.4,<3.0.0" },
|
||||
{ name = "chainlit", marker = "extra == 'code'", specifier = ">=2.9.4,<3.0.0" },
|
||||
{ name = "chainlit", marker = "extra == 'realtime'", specifier = ">=2.9.4,<3.0.0" },
|
||||
{ name = "chainlit", marker = "extra == 'ui'", specifier = ">=2.9.4,<3.0.0" },
|
||||
{ name = "chainlit", marker = "extra == 'chat'", specifier = ">=2.8.5,<2.9.0" },
|
||||
{ name = "chainlit", marker = "extra == 'code'", specifier = ">=2.8.5,<2.9.0" },
|
||||
{ name = "chainlit", marker = "extra == 'realtime'", specifier = ">=2.8.5,<2.9.0" },
|
||||
{ name = "chainlit", marker = "extra == 'ui'", specifier = ">=2.8.5,<2.9.0" },
|
||||
{ name = "crawl4ai", marker = "extra == 'chat'", specifier = ">=0.7.0" },
|
||||
{ name = "crawl4ai", marker = "extra == 'code'", specifier = ">=0.7.0" },
|
||||
{ name = "crawl4ai", marker = "extra == 'realtime'", specifier = ">=0.7.0" },
|
||||
@@ -4147,7 +4163,7 @@ requires-dist = [
|
||||
{ name = "websockets", marker = "extra == 'realtime'", specifier = ">=12.0" },
|
||||
{ name = "yfinance", marker = "extra == 'realtime'", specifier = ">=0.2.44" },
|
||||
]
|
||||
provides-extras = ["ui", "gradio", "api", "agentops", "google", "openai", "anthropic", "cohere", "chat", "code", "realtime", "call", "train", "crewai", "autogen", "autogen-v4"]
|
||||
provides-extras = ["ui", "gradio", "api", "agentops", "google", "openai", "anthropic", "cohere", "chat", "code", "realtime", "call", "train", "crewai", "autogen", "autogen-v4", "acp"]
|
||||
|
||||
[[package]]
|
||||
name = "praisonai-tools"
|
||||
@@ -6026,105 +6042,26 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.1.1"
|
||||
version = "0.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/48/02d2d2cbf54e134810b2cb40ac79fdb8ce08476184536a4764717a7bc9f4/watchfiles-0.20.0.tar.gz", hash = "sha256:728575b6b94c90dd531514677201e8851708e6e4b5fe7028ac506a200b622019", size = 37041, upload-time = "2023-08-24T12:49:17.616Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/db/899832e11fef2d468bf8b3c1c13289b1db4cb7c3410bb2a9612a52fc8b22/watchfiles-0.20.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:3796312bd3587e14926013612b23066912cf45a14af71cf2b20db1c12dadf4e9", size = 417357, upload-time = "2023-08-24T12:48:43.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/1a/85c914e4db62a3f8197daa98a271ea380a5d200a8d3058bd9f417752bc26/watchfiles-0.20.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:d0002d81c89a662b595645fb684a371b98ff90a9c7d8f8630c82f0fde8310458", size = 407258, upload-time = "2023-08-24T12:48:45.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/ae/b7bddad421af5e33079a2ce639aa58837b715a2da98df16e25ecd310af52/watchfiles-0.20.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:570848706440373b4cd8017f3e850ae17f76dbdf1e9045fc79023b11e1afe490", size = 1331327, upload-time = "2023-08-24T12:48:47.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/e5/b080cec4e841b1cf338ccbd958cf3232ad1691a590653b2d124b5c79cf6b/watchfiles-0.20.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a0351d20d03c6f7ad6b2e8a226a5efafb924c7755ee1e34f04c77c3682417fa", size = 1301371, upload-time = "2023-08-24T12:48:48.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/a0/2fb2c36730995a6b3f060187195dc08ad9ceee67426bdca8a4296024071c/watchfiles-0.20.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:007dcc4a401093010b389c044e81172c8a2520dba257c88f8828b3d460c6bb38", size = 1302438, upload-time = "2023-08-24T12:48:49.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/ea/d11971958ae703cfe443b21f672169cb8bc12dbec5781b910633fa2186ec/watchfiles-0.20.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d82dbc1832da83e441d112069833eedd4cf583d983fb8dd666fbefbea9d99c0", size = 1410655, upload-time = "2023-08-24T12:48:51.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/81/3f922f3ede53ca9c0b4095f63688ffeea19a49592d0ac62db1eb9632b1e3/watchfiles-0.20.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99f4c65fd2fce61a571b2a6fcf747d6868db0bef8a934e8ca235cc8533944d95", size = 1494222, upload-time = "2023-08-24T12:48:54.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/46/c9d5ee4871b187d291d62e61c41f9a4d67d4866a89704b0ad16b6949e9bd/watchfiles-0.20.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5392dd327a05f538c56edb1c6ebba6af91afc81b40822452342f6da54907bbdf", size = 1294171, upload-time = "2023-08-24T12:48:56.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/5e/6b64e3bf9fd4422250f3c716d992dd76dbe55e6fa1e7ebaf2bf88f389707/watchfiles-0.20.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:08dc702529bb06a2b23859110c214db245455532da5eaea602921687cfcd23db", size = 1462256, upload-time = "2023-08-24T12:48:57.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c0/75f5a71ac24118ab11bd898e0114cedc72b25924ff2d960d473bddb4ec6e/watchfiles-0.20.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7d4e66a857621584869cfbad87039e65dadd7119f0d9bb9dbc957e089e32c164", size = 1461725, upload-time = "2023-08-24T12:48:59.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/d4/0c0fdcc4293ad1b73db54896fa0de4b37439ae4f25971b5eb1708dd04f9a/watchfiles-0.20.0-cp37-abi3-win32.whl", hash = "sha256:a03d1e6feb7966b417f43c3e3783188167fd69c2063e86bad31e62c4ea794cc5", size = 268193, upload-time = "2023-08-24T12:49:01.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/79/098b1b1fcb6de16149d23283a2ab5dadce6a06b864e7a182d231f57a1f9e/watchfiles-0.20.0-cp37-abi3-win_amd64.whl", hash = "sha256:eccc8942bcdc7d638a01435d915b913255bbd66f018f1af051cd8afddb339ea3", size = 276723, upload-time = "2023-08-24T12:49:02.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/82/45dddf4f5bf8b73ba27382cebb2bb3c0ee922c7ef77d936b86276aa39dca/watchfiles-0.20.0-cp37-abi3-win_arm64.whl", hash = "sha256:b17d4176c49d207865630da5b59a91779468dd3e08692fe943064da260de2c7c", size = 265344, upload-time = "2023-08-24T12:49:04.107Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user