mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 17:31:00 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dbf194312 | ||
|
|
335e8d7e47 | ||
|
|
d65840e092 |
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.3.75" \
|
||||
"praisonai>=2.3.78" \
|
||||
"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.3.75" \
|
||||
"praisonai>=2.3.78" \
|
||||
"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.3.75" \
|
||||
"praisonai>=2.3.78" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import sys
|
||||
warnings.filterwarnings("ignore", message=".*There is no current event loop.*", category=DeprecationWarning)
|
||||
warnings.filterwarnings("ignore", message=".*Pydantic serializer warnings.*")
|
||||
warnings.filterwarnings("ignore", message=".*PydanticSerializationUnexpectedValue.*")
|
||||
warnings.filterwarnings("ignore", message=".*Expected 9 fields but got.*")
|
||||
warnings.filterwarnings("ignore", message=".*Expected \\d+ fields but got.*")
|
||||
warnings.filterwarnings("ignore", message=".*Expected `StreamingChoices` but got.*")
|
||||
warnings.filterwarnings("ignore", message=".*serialized value may not be as expected.*")
|
||||
warnings.filterwarnings("ignore", message=".*Use 'content=<...>' to upload raw bytes/text content.*")
|
||||
@@ -28,7 +28,7 @@ SUPPRESSED_PATTERNS = [
|
||||
"The `dict` method is deprecated; use `model_dump` instead",
|
||||
"Pydantic serializer warnings",
|
||||
"PydanticSerializationUnexpectedValue",
|
||||
"Expected 9 fields but got 5 for type `Message`",
|
||||
"Expected", # Catches all "Expected N fields but got M" patterns
|
||||
"Expected `StreamingChoices` but got `Choices`",
|
||||
"serialized value may not be as expected",
|
||||
"Mixing V1 models and V2 models",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -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.3.75.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.75.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.78.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.78.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -105,6 +105,7 @@ class FileSearchService:
|
||||
- Fuzzy match file paths
|
||||
- Cache results with TTL
|
||||
- Respect .gitignore (basic)
|
||||
- Support absolute paths (@/Users/...) and home paths (@~/...)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -125,9 +126,10 @@ class FileSearchService:
|
||||
# Cache: query -> (results, timestamp)
|
||||
self._cache: Dict[str, Tuple[List[FileSuggestion], float]] = {}
|
||||
|
||||
# File list cache
|
||||
# File list cache per directory
|
||||
self._file_list: Optional[List[str]] = None
|
||||
self._file_list_time: float = 0
|
||||
self._file_list_dir: Optional[str] = None
|
||||
|
||||
def _should_ignore(self, name: str) -> bool:
|
||||
"""Check if file/dir should be ignored."""
|
||||
@@ -136,20 +138,23 @@ class FileSearchService:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_file_list(self) -> List[str]:
|
||||
def _get_file_list(self, search_dir: str = None) -> List[str]:
|
||||
"""Get list of all files (cached)."""
|
||||
now = time.time()
|
||||
target_dir = search_dir or self._root
|
||||
|
||||
# Return cached if fresh
|
||||
if self._file_list is not None and (now - self._file_list_time) < self._cache_ttl:
|
||||
# Return cached if fresh and same directory
|
||||
if (self._file_list is not None and
|
||||
self._file_list_dir == target_dir and
|
||||
(now - self._file_list_time) < self._cache_ttl):
|
||||
return self._file_list
|
||||
|
||||
files = []
|
||||
|
||||
try:
|
||||
for root, dirs, filenames in os.walk(self._root):
|
||||
for root, dirs, filenames in os.walk(target_dir):
|
||||
# Calculate depth
|
||||
rel_root = os.path.relpath(root, self._root)
|
||||
rel_root = os.path.relpath(root, target_dir)
|
||||
depth = 0 if rel_root == '.' else rel_root.count(os.sep) + 1
|
||||
|
||||
if depth > self._max_depth:
|
||||
@@ -161,19 +166,20 @@ class FileSearchService:
|
||||
|
||||
# Add directories
|
||||
for d in dirs:
|
||||
rel_path = os.path.relpath(os.path.join(root, d), self._root)
|
||||
rel_path = os.path.relpath(os.path.join(root, d), target_dir)
|
||||
files.append(rel_path + os.sep)
|
||||
|
||||
# Add files
|
||||
for f in filenames:
|
||||
if not self._should_ignore(f):
|
||||
rel_path = os.path.relpath(os.path.join(root, f), self._root)
|
||||
rel_path = os.path.relpath(os.path.join(root, f), target_dir)
|
||||
files.append(rel_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self._file_list = files
|
||||
self._file_list_time = now
|
||||
self._file_list_dir = target_dir
|
||||
return files
|
||||
|
||||
def _fuzzy_match(self, text: str, query: str) -> int:
|
||||
@@ -221,6 +227,11 @@ class FileSearchService:
|
||||
"""
|
||||
Search for files matching query.
|
||||
|
||||
Supports:
|
||||
- Relative paths: @file.txt, @src/main.py
|
||||
- Home paths: @~/Documents, @~/.config
|
||||
- Absolute paths: @/Users/name/file.txt, @/etc/hosts
|
||||
|
||||
Args:
|
||||
query: Search query (fuzzy matched)
|
||||
max_results: Maximum results to return
|
||||
@@ -228,8 +239,51 @@ class FileSearchService:
|
||||
Returns:
|
||||
List of FileSuggestion sorted by score
|
||||
"""
|
||||
# Determine search directory based on query
|
||||
search_dir = self._root
|
||||
search_query = query
|
||||
prefix = ""
|
||||
|
||||
# Handle home directory paths (@~/)
|
||||
if query.startswith('~'):
|
||||
expanded = os.path.expanduser(query)
|
||||
# Find the directory part and filename part
|
||||
if os.path.isdir(expanded):
|
||||
search_dir = expanded
|
||||
search_query = ""
|
||||
prefix = query.rstrip('/') + '/'
|
||||
else:
|
||||
parent = os.path.dirname(expanded)
|
||||
if os.path.isdir(parent):
|
||||
search_dir = parent
|
||||
search_query = os.path.basename(expanded)
|
||||
prefix = os.path.dirname(query).rstrip('/') + '/'
|
||||
else:
|
||||
# Just expand ~ and search from home
|
||||
search_dir = os.path.expanduser('~')
|
||||
search_query = query[2:] if query.startswith('~/') else query[1:]
|
||||
prefix = "~/"
|
||||
|
||||
# Handle absolute paths (@/Users/..., @/etc/...)
|
||||
elif query.startswith('/'):
|
||||
if os.path.isdir(query):
|
||||
search_dir = query
|
||||
search_query = ""
|
||||
prefix = query.rstrip('/') + '/'
|
||||
else:
|
||||
parent = os.path.dirname(query)
|
||||
if os.path.isdir(parent):
|
||||
search_dir = parent
|
||||
search_query = os.path.basename(query)
|
||||
prefix = parent.rstrip('/') + '/'
|
||||
else:
|
||||
# Search from root with the query
|
||||
search_dir = '/'
|
||||
search_query = query[1:]
|
||||
prefix = "/"
|
||||
|
||||
# Check cache
|
||||
cache_key = f"{query}:{max_results}"
|
||||
cache_key = f"{search_dir}:{search_query}:{max_results}"
|
||||
now = time.time()
|
||||
|
||||
if cache_key in self._cache:
|
||||
@@ -237,25 +291,26 @@ class FileSearchService:
|
||||
if (now - cached_time) < self._cache_ttl:
|
||||
return results
|
||||
|
||||
# Get all files
|
||||
all_files = self._get_file_list()
|
||||
# Get all files from the appropriate directory
|
||||
all_files = self._get_file_list(search_dir)
|
||||
|
||||
# Score and filter
|
||||
scored: List[Tuple[int, str]] = []
|
||||
for path in all_files:
|
||||
score = self._fuzzy_match(path, query)
|
||||
score = self._fuzzy_match(path, search_query)
|
||||
if score > 0:
|
||||
scored.append((score, path))
|
||||
|
||||
# Sort by score descending
|
||||
scored.sort(key=lambda x: (-x[0], x[1]))
|
||||
|
||||
# Build results
|
||||
# Build results with proper prefix
|
||||
results = []
|
||||
for score, path in scored[:max_results]:
|
||||
file_type = "directory" if path.endswith(os.sep) else "file"
|
||||
display_path = prefix + path if prefix else path
|
||||
results.append(FileSuggestion(
|
||||
path=path,
|
||||
path=display_path,
|
||||
file_type=file_type,
|
||||
score=score
|
||||
))
|
||||
|
||||
@@ -45,8 +45,10 @@ class AutoMemoryHandler(FlagHandler):
|
||||
def _get_auto_memory(self, user_id: str = None):
|
||||
"""Get AutoMemory instance lazily."""
|
||||
try:
|
||||
from praisonaiagents.memory import AutoMemory
|
||||
return AutoMemory(user_id=user_id or "default")
|
||||
from praisonaiagents.memory import AutoMemory, FileMemory
|
||||
# AutoMemory requires a FileMemory instance
|
||||
memory = FileMemory(user_id=user_id or "default")
|
||||
return AutoMemory(memory=memory)
|
||||
except ImportError:
|
||||
self.print_status(
|
||||
"AutoMemory requires praisonaiagents. Install with: pip install praisonaiagents",
|
||||
@@ -71,33 +73,36 @@ class AutoMemoryHandler(FlagHandler):
|
||||
config['auto_memory_user_id'] = flag_value.get('user_id', 'default')
|
||||
return config
|
||||
|
||||
def extract_memories(self, text: str, user_id: str = None) -> Dict[str, Any]:
|
||||
def extract_memories(self, text: str, user_id: str = None, user_message: str = None) -> list:
|
||||
"""
|
||||
Extract memories from text.
|
||||
Extract and store memories from text.
|
||||
|
||||
Args:
|
||||
text: Text to extract memories from
|
||||
text: Assistant response text to extract memories from
|
||||
user_id: User ID for memory isolation
|
||||
user_message: Original user message (for context)
|
||||
|
||||
Returns:
|
||||
Dictionary of extracted memories
|
||||
List of extracted memories
|
||||
"""
|
||||
auto_memory = self._get_auto_memory(user_id)
|
||||
if not auto_memory:
|
||||
return {}
|
||||
return []
|
||||
|
||||
try:
|
||||
if hasattr(auto_memory, 'extract'):
|
||||
memories = auto_memory.extract(text)
|
||||
self.print_status(f"🧠 Extracted {len(memories)} memories", "success")
|
||||
return memories
|
||||
elif hasattr(auto_memory, 'process'):
|
||||
result = auto_memory.process(text)
|
||||
return result
|
||||
# Use process_interaction which extracts AND stores memories
|
||||
memories = auto_memory.process_interaction(
|
||||
user_message=user_message or text,
|
||||
assistant_response=text if user_message else None,
|
||||
store=True # Store the memories
|
||||
)
|
||||
if memories:
|
||||
self.print_status(f"🧠 Extracted and stored {len(memories)} memories", "success")
|
||||
return memories
|
||||
except Exception as e:
|
||||
self.log(f"Memory extraction failed: {e}", "error")
|
||||
|
||||
return {}
|
||||
return []
|
||||
|
||||
def store_memory(self, content: str, user_id: str = None, importance: float = 0.5) -> bool:
|
||||
"""
|
||||
@@ -127,13 +132,14 @@ class AutoMemoryHandler(FlagHandler):
|
||||
self.log(f"Memory storage failed: {e}", "error")
|
||||
return False
|
||||
|
||||
def post_process_result(self, result: Any, flag_value: Any) -> Any:
|
||||
def post_process_result(self, result: Any, flag_value: Any, user_message: str = None) -> Any:
|
||||
"""
|
||||
Post-process result to extract and store memories.
|
||||
|
||||
Args:
|
||||
result: Agent output
|
||||
flag_value: Boolean or dict with configuration
|
||||
user_message: Original user message for context
|
||||
|
||||
Returns:
|
||||
Original result (memories are stored)
|
||||
@@ -144,15 +150,18 @@ class AutoMemoryHandler(FlagHandler):
|
||||
user_id = None
|
||||
if isinstance(flag_value, dict):
|
||||
user_id = flag_value.get('user_id')
|
||||
user_message = flag_value.get('user_message', user_message)
|
||||
|
||||
# Extract memories from result
|
||||
# Extract and store memories from result
|
||||
text = str(result)
|
||||
memories = self.extract_memories(text, user_id)
|
||||
memories = self.extract_memories(text, user_id, user_message=user_message)
|
||||
|
||||
if memories:
|
||||
self.print_status("\n🧠 Auto-extracted Memories:", "info")
|
||||
for key, value in memories.items():
|
||||
self.print_status(f" • {key}: {value[:50]}...", "info")
|
||||
for mem in memories:
|
||||
mem_type = mem.get('type', 'unknown')
|
||||
content = mem.get('content', str(mem))[:50]
|
||||
self.print_status(f" • {mem_type}: {content}...", "info")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
COMPARE_MODES = {
|
||||
"basic": {},
|
||||
"tools": {"tools": "internet_search"},
|
||||
"tools": {"tools": "calculator,internet_search"},
|
||||
"research": {"research": True},
|
||||
"planning": {"planning": True},
|
||||
"memory": {"memory": True},
|
||||
@@ -407,13 +407,7 @@ class CompareHandler:
|
||||
from praisonaiagents.tools import internet_search
|
||||
tools.append(internet_search)
|
||||
elif name == "calculator":
|
||||
def calculator(expression: str) -> str:
|
||||
"""Evaluate a mathematical expression."""
|
||||
try:
|
||||
return str(eval(expression))
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
tools.append(calculator)
|
||||
tools.append(_calculator_tool)
|
||||
except ImportError:
|
||||
logger.warning(f"Could not load tool: {name}")
|
||||
|
||||
@@ -466,3 +460,50 @@ class CompareHandler:
|
||||
print(f"[red]Failed to save results to: {output_path}[/red]")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _calculator_tool(expression: str) -> str:
|
||||
"""
|
||||
Evaluate a mathematical expression safely.
|
||||
|
||||
Args:
|
||||
expression: Mathematical expression to evaluate (e.g., "2+2", "15*3", "100/4")
|
||||
|
||||
Returns:
|
||||
The result of the calculation as a string
|
||||
"""
|
||||
import ast
|
||||
import operator
|
||||
|
||||
# Safe operators
|
||||
operators = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
ast.Pow: operator.pow,
|
||||
ast.USub: operator.neg,
|
||||
ast.UAdd: operator.pos,
|
||||
ast.Mod: operator.mod,
|
||||
ast.FloorDiv: operator.floordiv,
|
||||
}
|
||||
|
||||
def _eval(node):
|
||||
if isinstance(node, ast.Constant):
|
||||
return node.value
|
||||
elif isinstance(node, ast.BinOp):
|
||||
left = _eval(node.left)
|
||||
right = _eval(node.right)
|
||||
return operators[type(node.op)](left, right)
|
||||
elif isinstance(node, ast.UnaryOp):
|
||||
operand = _eval(node.operand)
|
||||
return operators[type(node.op)](operand)
|
||||
else:
|
||||
raise ValueError(f"Unsupported operation: {type(node)}")
|
||||
|
||||
try:
|
||||
tree = ast.parse(expression, mode='eval')
|
||||
result = _eval(tree.body)
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error evaluating '{expression}': {e}"
|
||||
@@ -26,42 +26,68 @@ class EvalHandler:
|
||||
|
||||
def run_accuracy(
|
||||
self,
|
||||
agent_file: str,
|
||||
input_text: str,
|
||||
expected_output: str,
|
||||
agent_file: Optional[str] = None,
|
||||
input_text: str = "",
|
||||
expected_output: str = "",
|
||||
iterations: int = 1,
|
||||
model: Optional[str] = None,
|
||||
output_file: Optional[str] = None
|
||||
output_file: Optional[str] = None,
|
||||
prompt: Optional[str] = None,
|
||||
llm: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run accuracy evaluation on an agent.
|
||||
|
||||
Args:
|
||||
agent_file: Path to agents.yaml file
|
||||
agent_file: Path to agents.yaml file (optional if prompt is provided)
|
||||
input_text: Input to provide to the agent
|
||||
expected_output: Expected output to compare against
|
||||
iterations: Number of evaluation iterations
|
||||
model: LLM model for judging
|
||||
output_file: Path to save results
|
||||
prompt: Direct prompt (alternative to agent_file)
|
||||
llm: LLM model for the agent (when using prompt)
|
||||
|
||||
Returns:
|
||||
Evaluation result dictionary
|
||||
"""
|
||||
try:
|
||||
from praisonaiagents.eval import AccuracyEvaluator
|
||||
from praisonai.agents_generator import AgentsGenerator
|
||||
from praisonaiagents import Agent
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import evaluation modules: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
try:
|
||||
generator = AgentsGenerator(agent_file)
|
||||
agents = generator.generate_agents()
|
||||
|
||||
if not agents:
|
||||
return {"error": "No agents found in configuration"}
|
||||
|
||||
agent = agents[0] if isinstance(agents, list) else agents
|
||||
# Create agent either from file or from prompt
|
||||
if prompt:
|
||||
# Direct prompt mode - create agent on the fly
|
||||
agent = Agent(
|
||||
name="EvalAgent",
|
||||
role="Assistant",
|
||||
goal="Complete the given task",
|
||||
backstory="You are a helpful assistant.",
|
||||
llm=llm or model or "gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
# Use prompt as input if input_text not provided
|
||||
if not input_text:
|
||||
input_text = prompt
|
||||
elif agent_file:
|
||||
# Load from agents.yaml
|
||||
try:
|
||||
from praisonai.agents_generator import AgentsGenerator
|
||||
generator = AgentsGenerator(agent_file)
|
||||
agents = generator.generate_agents()
|
||||
|
||||
if not agents:
|
||||
return {"error": "No agents found in configuration"}
|
||||
|
||||
agent = agents[0] if isinstance(agents, list) else agents
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to load agents from {agent_file}: {e}"}
|
||||
else:
|
||||
return {"error": "Either --agent or --prompt must be provided"}
|
||||
|
||||
evaluator = AccuracyEvaluator(
|
||||
agent=agent,
|
||||
@@ -331,8 +357,16 @@ def handle_eval_command(args) -> None:
|
||||
handler = EvalHandler(verbose=getattr(args, 'verbose', False))
|
||||
|
||||
eval_type = getattr(args, 'eval_type', 'accuracy')
|
||||
agent_file = getattr(args, 'agent', 'agents.yaml')
|
||||
agent_file = getattr(args, 'agent', None)
|
||||
output_file = getattr(args, 'output', None)
|
||||
prompt = getattr(args, 'prompt', None)
|
||||
llm = getattr(args, 'llm', None)
|
||||
|
||||
# If no agent file and no prompt, check if agents.yaml exists
|
||||
if not agent_file and not prompt:
|
||||
import os
|
||||
if os.path.exists('agents.yaml'):
|
||||
agent_file = 'agents.yaml'
|
||||
|
||||
if eval_type == 'accuracy':
|
||||
result = handler.run_accuracy(
|
||||
@@ -341,7 +375,9 @@ def handle_eval_command(args) -> None:
|
||||
expected_output=getattr(args, 'expected', ''),
|
||||
iterations=getattr(args, 'iterations', 1),
|
||||
model=getattr(args, 'model', None),
|
||||
output_file=output_file
|
||||
output_file=output_file,
|
||||
prompt=prompt,
|
||||
llm=llm
|
||||
)
|
||||
elif eval_type == 'performance':
|
||||
result = handler.run_performance(
|
||||
@@ -410,8 +446,10 @@ def add_eval_parser(subparsers) -> None:
|
||||
'accuracy',
|
||||
help='Run accuracy evaluation'
|
||||
)
|
||||
accuracy_parser.add_argument('--agent', '-a', default='agents.yaml', help='Agent config file')
|
||||
accuracy_parser.add_argument('--input', '-i', required=True, help='Input text')
|
||||
accuracy_parser.add_argument('--agent', '-a', help='Agent config file (optional if --prompt used)')
|
||||
accuracy_parser.add_argument('--prompt', '-p', help='Direct prompt (alternative to --agent)')
|
||||
accuracy_parser.add_argument('--llm', help='LLM model for agent (when using --prompt)')
|
||||
accuracy_parser.add_argument('--input', '-i', help='Input text (defaults to --prompt if not provided)')
|
||||
accuracy_parser.add_argument('--expected', '-e', required=True, help='Expected output')
|
||||
accuracy_parser.add_argument('--iterations', '-n', type=int, default=1, help='Number of iterations')
|
||||
accuracy_parser.add_argument('--model', '-m', help='Judge model')
|
||||
|
||||
@@ -3,12 +3,21 @@ Fast Context Handler for CLI.
|
||||
|
||||
Provides codebase search capability using FastContext.
|
||||
Usage: praisonai "Find authentication code" --fast-context ./src
|
||||
|
||||
Fast Context Strategy:
|
||||
1. Extract search keywords from natural language query using LLM
|
||||
2. Provide folder tree context to help understand codebase structure
|
||||
3. Use both file name search AND content search (ripgrep-style)
|
||||
4. Return relevant code snippets for context
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Any, Dict, Tuple, List
|
||||
from .base import FlagHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FastContextHandler(FlagHandler):
|
||||
"""
|
||||
@@ -61,14 +70,172 @@ class FastContextHandler(FlagHandler):
|
||||
|
||||
return True, ""
|
||||
|
||||
def _get_folder_tree(self, path: str, max_depth: int = 2) -> str:
|
||||
"""Get folder tree structure for context."""
|
||||
tree_lines = []
|
||||
path = os.path.abspath(path)
|
||||
|
||||
ignore_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv', '.tox', 'dist', 'build', '.egg-info'}
|
||||
ignore_exts = {'.pyc', '.pyo', '.so', '.o', '.a', '.dylib'}
|
||||
|
||||
def walk_tree(current_path, prefix="", depth=0):
|
||||
if depth > max_depth:
|
||||
return
|
||||
try:
|
||||
entries = sorted(os.listdir(current_path))
|
||||
except PermissionError:
|
||||
return
|
||||
|
||||
dirs = []
|
||||
files = []
|
||||
for entry in entries:
|
||||
full_path = os.path.join(current_path, entry)
|
||||
if os.path.isdir(full_path):
|
||||
if entry not in ignore_dirs and not entry.startswith('.'):
|
||||
dirs.append(entry)
|
||||
else:
|
||||
ext = os.path.splitext(entry)[1]
|
||||
if ext not in ignore_exts and not entry.startswith('.'):
|
||||
files.append(entry)
|
||||
|
||||
for i, d in enumerate(dirs):
|
||||
is_last = (i == len(dirs) - 1) and not files
|
||||
tree_lines.append(f"{prefix}{'└── ' if is_last else '├── '}{d}/")
|
||||
walk_tree(os.path.join(current_path, d),
|
||||
prefix + (' ' if is_last else '│ '), depth + 1)
|
||||
|
||||
for i, f in enumerate(files[:10]): # Limit files shown
|
||||
is_last = i == len(files[:10]) - 1
|
||||
tree_lines.append(f"{prefix}{'└── ' if is_last else '├── '}{f}")
|
||||
if len(files) > 10:
|
||||
tree_lines.append(f"{prefix} ... and {len(files) - 10} more files")
|
||||
|
||||
tree_lines.append(os.path.basename(path) + "/")
|
||||
walk_tree(path)
|
||||
return "\n".join(tree_lines[:50]) # Limit total lines
|
||||
|
||||
def _extract_keywords(self, query: str, folder_tree: str) -> List[str]:
|
||||
"""Extract search keywords from natural language query using LLM."""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
|
||||
extractor = Agent(
|
||||
name="KeywordExtractor",
|
||||
role="Search Query Analyzer",
|
||||
goal="Extract file search keywords from natural language",
|
||||
backstory="Expert at understanding code search queries",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False
|
||||
)
|
||||
|
||||
prompt = f"""Given this folder structure:
|
||||
{folder_tree}
|
||||
|
||||
And this search query: "{query}"
|
||||
|
||||
Extract 1-5 specific keywords/patterns to search for in file names and content.
|
||||
Return ONLY the keywords, one per line, no explanations.
|
||||
Focus on: file names, function names, class names, variable names that might match.
|
||||
If the query mentions a folder name that exists, include files from that folder."""
|
||||
|
||||
response = extractor.chat(prompt, stream=False)
|
||||
keywords = [k.strip() for k in str(response).strip().split('\n') if k.strip()]
|
||||
logger.debug(f"Extracted keywords: {keywords}")
|
||||
return keywords[:5] # Limit to 5 keywords
|
||||
except Exception as e:
|
||||
logger.debug(f"Keyword extraction failed: {e}")
|
||||
# Fallback: extract words from query
|
||||
return [w for w in query.split() if len(w) > 2][:3]
|
||||
|
||||
def _search_files_by_name(self, path: str, keywords: List[str]) -> List[Dict[str, Any]]:
|
||||
"""Search for files matching keywords in their names."""
|
||||
matches = []
|
||||
path = os.path.abspath(path)
|
||||
|
||||
ignore_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv'}
|
||||
|
||||
for root, dirs, files in os.walk(path):
|
||||
dirs[:] = [d for d in dirs if d not in ignore_dirs]
|
||||
|
||||
for filename in files:
|
||||
filepath = os.path.join(root, filename)
|
||||
rel_path = os.path.relpath(filepath, path)
|
||||
|
||||
# Check if any keyword matches filename or path
|
||||
for kw in keywords:
|
||||
kw_lower = kw.lower()
|
||||
if kw_lower in filename.lower() or kw_lower in rel_path.lower():
|
||||
matches.append({
|
||||
'file': rel_path,
|
||||
'lines': [(1, 50)], # First 50 lines
|
||||
'relevance': 1.0,
|
||||
'match_type': 'filename'
|
||||
})
|
||||
break
|
||||
|
||||
return matches[:20] # Limit results
|
||||
|
||||
def _search_files_by_content(self, path: str, keywords: List[str]) -> List[Dict[str, Any]]:
|
||||
"""Search for files containing keywords in content."""
|
||||
matches = []
|
||||
path = os.path.abspath(path)
|
||||
|
||||
ignore_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv'}
|
||||
code_exts = {'.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.go', '.rs', '.rb', '.sh', '.yaml', '.yml', '.json', '.md', '.txt'}
|
||||
|
||||
for root, dirs, files in os.walk(path):
|
||||
dirs[:] = [d for d in dirs if d not in ignore_dirs]
|
||||
|
||||
for filename in files:
|
||||
ext = os.path.splitext(filename)[1]
|
||||
if ext not in code_exts:
|
||||
continue
|
||||
|
||||
filepath = os.path.join(root, filename)
|
||||
rel_path = os.path.relpath(filepath, path)
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read(50000) # Read first 50KB
|
||||
|
||||
content_lower = content.lower()
|
||||
for kw in keywords:
|
||||
if kw.lower() in content_lower:
|
||||
# Find line numbers with matches
|
||||
lines = content.split('\n')
|
||||
match_lines = []
|
||||
for i, line in enumerate(lines[:200], 1): # Check first 200 lines
|
||||
if kw.lower() in line.lower():
|
||||
match_lines.append((max(1, i-2), min(len(lines), i+2)))
|
||||
|
||||
if match_lines:
|
||||
matches.append({
|
||||
'file': rel_path,
|
||||
'lines': match_lines[:5], # Limit line ranges
|
||||
'relevance': 0.8,
|
||||
'match_type': 'content'
|
||||
})
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return matches[:15] # Limit results
|
||||
|
||||
def search_context(self, query: str, path: str, **kwargs) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search for relevant context in the codebase.
|
||||
|
||||
Uses a smart multi-step approach:
|
||||
1. Get folder tree structure
|
||||
2. Use LLM to extract search keywords from query
|
||||
3. Search by filename AND content
|
||||
4. Return combined results
|
||||
|
||||
Args:
|
||||
query: Search query
|
||||
query: Natural language search query
|
||||
path: Path to search
|
||||
**kwargs: Additional search options
|
||||
- use_llm_keywords: Use LLM to extract keywords (default: True)
|
||||
|
||||
Returns:
|
||||
List of matching results
|
||||
@@ -84,32 +251,50 @@ class FastContextHandler(FlagHandler):
|
||||
return []
|
||||
|
||||
try:
|
||||
from praisonaiagents import FastContext
|
||||
use_llm_keywords = kwargs.get('use_llm_keywords', True)
|
||||
|
||||
fc = FastContext(
|
||||
path=path,
|
||||
model=kwargs.get('model', 'gpt-4o-mini'),
|
||||
max_turns=kwargs.get('max_turns', 4),
|
||||
parallelism=kwargs.get('parallelism', 8),
|
||||
timeout=kwargs.get('timeout', 30.0)
|
||||
)
|
||||
# Step 1: Get folder tree for context
|
||||
logger.debug(f"Getting folder tree for: {path}")
|
||||
folder_tree = self._get_folder_tree(path)
|
||||
logger.debug(f"Folder tree:\n{folder_tree}")
|
||||
|
||||
result = fc.search(query)
|
||||
# Step 2: Extract keywords
|
||||
if use_llm_keywords:
|
||||
logger.debug(f"Extracting keywords from query: {query}")
|
||||
keywords = self._extract_keywords(query, folder_tree)
|
||||
else:
|
||||
keywords = [w for w in query.split() if len(w) > 2][:3]
|
||||
|
||||
# Convert to list of dicts
|
||||
logger.debug(f"Search keywords: {keywords}")
|
||||
|
||||
if not keywords:
|
||||
self.print_status("Could not extract search keywords", "warning")
|
||||
return []
|
||||
|
||||
# Step 3: Search by filename
|
||||
logger.debug("Searching by filename...")
|
||||
filename_matches = self._search_files_by_name(path, keywords)
|
||||
logger.debug(f"Found {len(filename_matches)} filename matches")
|
||||
|
||||
# Step 4: Search by content
|
||||
logger.debug("Searching by content...")
|
||||
content_matches = self._search_files_by_content(path, keywords)
|
||||
logger.debug(f"Found {len(content_matches)} content matches")
|
||||
|
||||
# Combine and deduplicate
|
||||
seen_files = set()
|
||||
matches = []
|
||||
if hasattr(result, 'files'):
|
||||
for file_match in result.files:
|
||||
matches.append({
|
||||
'file': file_match.path,
|
||||
'lines': [(lr.start, lr.end) for lr in file_match.line_ranges],
|
||||
'relevance': getattr(file_match, 'relevance', 1.0)
|
||||
})
|
||||
|
||||
for m in filename_matches + content_matches:
|
||||
if m['file'] not in seen_files:
|
||||
seen_files.add(m['file'])
|
||||
matches.append(m)
|
||||
|
||||
self.print_status(f"🔍 Found {len(matches)} relevant files", "success")
|
||||
return matches
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"FastContext search error: {e}")
|
||||
self.log(f"FastContext search error: {e}", "error")
|
||||
return []
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class GuardrailHandler(FlagHandler):
|
||||
|
||||
Args:
|
||||
description: Natural language description of validation criteria
|
||||
llm: Optional LLM instance for validation
|
||||
llm: Optional LLM instance for validation (defaults to gpt-4o-mini)
|
||||
|
||||
Returns:
|
||||
LLMGuardrail instance or None if unavailable
|
||||
@@ -87,6 +87,12 @@ class GuardrailHandler(FlagHandler):
|
||||
)
|
||||
return None
|
||||
|
||||
# Provide default LLM if none specified
|
||||
if llm is None:
|
||||
import os
|
||||
# Try to get from environment or use default
|
||||
llm = os.environ.get('OPENAI_MODEL_NAME', 'gpt-4o-mini')
|
||||
|
||||
return LLMGuardrail(description=description, llm=llm)
|
||||
|
||||
def validate_output(self, output: str, description: str, llm: Any = None) -> Tuple[bool, str]:
|
||||
|
||||
@@ -95,9 +95,9 @@ class HandoffHandler(FlagHandler):
|
||||
for i, agent in enumerate(agents[:-1]):
|
||||
next_agent = agents[i + 1]
|
||||
handoff = Handoff(
|
||||
target=next_agent,
|
||||
name=f"handoff_to_{agent_names[i + 1]}",
|
||||
description=f"Hand off task to {agent_names[i + 1]} for further processing"
|
||||
agent=next_agent,
|
||||
tool_name_override=f"handoff_to_{agent_names[i + 1]}",
|
||||
tool_description_override=f"Hand off task to {agent_names[i + 1]} for further processing"
|
||||
)
|
||||
|
||||
# Add handoff to agent
|
||||
|
||||
@@ -5,6 +5,42 @@ import argparse
|
||||
import warnings
|
||||
import os
|
||||
|
||||
# Suppress Pydantic serialization warnings from LiteLLM BEFORE any imports
|
||||
# These warnings occur when LiteLLM's response objects have field mismatches
|
||||
# Using both filterwarnings AND patching warnings.warn for complete suppression
|
||||
|
||||
warnings.filterwarnings("ignore", message=".*Pydantic serializer warnings.*")
|
||||
warnings.filterwarnings("ignore", message=".*PydanticSerializationUnexpectedValue.*")
|
||||
warnings.filterwarnings("ignore", message=".*Expected \\d+ fields but got.*")
|
||||
warnings.filterwarnings("ignore", message=".*Expected `StreamingChoices`.*")
|
||||
warnings.filterwarnings("ignore", message=".*Expected `Message`.*")
|
||||
warnings.filterwarnings("ignore", message=".*serialized value may not be as expected.*")
|
||||
warnings.filterwarnings("ignore", category=UserWarning, module="pydantic.*")
|
||||
|
||||
# Patch warnings.showwarning to intercept ALL warnings including those from crewai's patched warn
|
||||
# This is the final output function that actually displays warnings
|
||||
_SUPPRESSED_PATTERNS = [
|
||||
"Pydantic serializer warnings",
|
||||
"PydanticSerializationUnexpectedValue",
|
||||
"Expected", # Catches "Expected N fields but got M"
|
||||
"StreamingChoices",
|
||||
"serialized value may not be as expected",
|
||||
"duckduckgo_search", # Suppress duckduckgo rename warning
|
||||
]
|
||||
|
||||
_original_showwarning = warnings.showwarning
|
||||
|
||||
def _patched_showwarning(message, category, filename, lineno, file=None, line=None):
|
||||
msg_str = str(message)
|
||||
for pattern in _SUPPRESSED_PATTERNS:
|
||||
if pattern in msg_str:
|
||||
return
|
||||
if category is UserWarning and "pydantic" in filename.lower():
|
||||
return
|
||||
_original_showwarning(message, category, filename, lineno, file, line)
|
||||
|
||||
warnings.showwarning = _patched_showwarning
|
||||
|
||||
# Suppress crewai RuntimeWarning about module loading order (only in non-debug mode)
|
||||
# This warning is harmless and occurs when running as `python -m praisonai.cli.main`
|
||||
if os.environ.get('LOGLEVEL', 'INFO').upper() != 'DEBUG':
|
||||
@@ -277,6 +313,18 @@ class PraisonAI:
|
||||
elif hasattr(args, 'direct_prompt') and args.direct_prompt:
|
||||
# Only handle direct prompt if agent_file wasn't explicitly set in constructor
|
||||
if original_agent_file == "agents.yaml": # Default value, so safe to use direct prompt
|
||||
# Handle --compare flag for CLI mode comparison
|
||||
if hasattr(args, 'compare') and args.compare:
|
||||
from .features.compare import CompareHandler
|
||||
handler = CompareHandler(verbose=getattr(args, 'verbose', False))
|
||||
result = handler.execute(
|
||||
args.direct_prompt,
|
||||
args.compare,
|
||||
model=getattr(args, 'llm', None),
|
||||
output_path=getattr(args, 'compare_output', None)
|
||||
)
|
||||
return result
|
||||
|
||||
# Combine direct prompt with any available inputs (stdin and/or file)
|
||||
prompt_parts = [args.direct_prompt]
|
||||
if stdin_input:
|
||||
@@ -1342,12 +1390,47 @@ class PraisonAI:
|
||||
|
||||
console.print(table)
|
||||
|
||||
# Show recent memories
|
||||
# Show recent short-term memories
|
||||
print("\n[bold]Recent Short-term Memories:[/bold]")
|
||||
short_term = memory.get_short_term(limit=5)
|
||||
for i, item in enumerate(short_term, 1):
|
||||
content = item.get('content', str(item))[:100]
|
||||
print(f" {i}. {content}")
|
||||
if short_term:
|
||||
for i, item in enumerate(short_term, 1):
|
||||
content = item.get('content', str(item))[:100]
|
||||
print(f" {i}. {content}")
|
||||
else:
|
||||
print(" [dim]No short-term memories[/dim]")
|
||||
|
||||
# Show long-term memories
|
||||
print("\n[bold]Long-term Memories:[/bold]")
|
||||
long_term = memory.get_long_term(limit=10)
|
||||
if long_term:
|
||||
for i, item in enumerate(long_term, 1):
|
||||
# Handle both dict and MemoryItem objects
|
||||
if hasattr(item, 'content'):
|
||||
content = str(item.content)[:100]
|
||||
importance = getattr(item, 'importance', 0)
|
||||
else:
|
||||
content = item.get('content', str(item))[:100]
|
||||
importance = item.get('importance', 0)
|
||||
print(f" {i}. [{importance:.1f}] {content}")
|
||||
else:
|
||||
print(" [dim]No long-term memories[/dim]")
|
||||
|
||||
# Show entities
|
||||
print("\n[bold]Entities:[/bold]")
|
||||
entities = memory.get_all_entities()
|
||||
if entities:
|
||||
for entity in entities[:10]:
|
||||
# Handle both dict and Entity objects
|
||||
if hasattr(entity, 'name'):
|
||||
name = entity.name
|
||||
entity_type = getattr(entity, 'entity_type', 'unknown')
|
||||
else:
|
||||
name = entity.get('name', 'Unknown')
|
||||
entity_type = entity.get('entity_type', 'unknown')
|
||||
print(f" • {name} ({entity_type})")
|
||||
else:
|
||||
print(" [dim]No entities[/dim]")
|
||||
|
||||
elif action == 'add':
|
||||
if not action_args:
|
||||
@@ -3118,9 +3201,8 @@ Provide ONLY the commit message, no explanations."""
|
||||
telemetry = TelemetryHandler(verbose=getattr(self.args, 'verbose', False))
|
||||
telemetry.enable()
|
||||
|
||||
# Auto Memory - Automatic memory extraction
|
||||
# Auto Memory - Automatic memory extraction (handled post-processing, not as Agent param)
|
||||
if getattr(self.args, 'auto_memory', False):
|
||||
agent_config["auto_memory"] = True
|
||||
print("[bold cyan]Auto Memory enabled - will extract and store memories[/bold cyan]")
|
||||
|
||||
# MCP - Model Context Protocol tools
|
||||
@@ -3266,7 +3348,10 @@ Provide ONLY the commit message, no explanations."""
|
||||
if hasattr(self, 'args') and getattr(self.args, 'auto_memory', False):
|
||||
from .features.auto_memory import AutoMemoryHandler
|
||||
auto_mem = AutoMemoryHandler(verbose=getattr(self.args, 'verbose', False))
|
||||
auto_mem.post_process_result(result, {'user_id': getattr(self.args, 'user_id', None)})
|
||||
auto_mem.post_process_result(
|
||||
result,
|
||||
{'user_id': getattr(self.args, 'user_id', None), 'user_message': prompt}
|
||||
)
|
||||
|
||||
# Todo - Generate todo list from response
|
||||
if hasattr(self, 'args') and getattr(self.args, 'todo', False):
|
||||
@@ -4668,16 +4753,14 @@ Provide a concise summary (max 200 words):"""
|
||||
|
||||
def _process_interactive_prompt_async(self, prompt, tools_list, console, session_state=None):
|
||||
"""
|
||||
Process a prompt asynchronously with live status display.
|
||||
Process a prompt asynchronously with non-blocking status display.
|
||||
|
||||
Shows real-time status (tools, commands) while processing in background.
|
||||
Shows status updates without blocking user input.
|
||||
Allows user to queue more messages while processing.
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
from rich.live import Live
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
import sys
|
||||
from praisonai.cli.features.message_queue import ProcessingState
|
||||
|
||||
state_manager = session_state['state_manager']
|
||||
@@ -4689,8 +4772,11 @@ Provide a concise summary (max 200 words):"""
|
||||
live_status.clear()
|
||||
live_status.update_status("Thinking...")
|
||||
|
||||
# Print initial status (non-blocking - user can still type)
|
||||
console.print(f"[dim cyan]⏳ {live_status.current_status}[/dim cyan]")
|
||||
|
||||
# Result container for thread communication
|
||||
result_container = {'response': None, 'error': None, 'done': False}
|
||||
result_container = {'response': None, 'error': None, 'done': False, 'last_status': ''}
|
||||
|
||||
def process_in_background():
|
||||
"""Run agent processing in background thread."""
|
||||
@@ -4736,37 +4822,36 @@ Provide a concise summary (max 200 words):"""
|
||||
bg_thread = threading.Thread(target=process_in_background, daemon=True)
|
||||
bg_thread.start()
|
||||
|
||||
# Show live status while processing
|
||||
try:
|
||||
with Live(console=console, refresh_per_second=4, transient=True) as live:
|
||||
while not result_container['done']:
|
||||
# Build status display
|
||||
status_text = Text()
|
||||
status_text.append("⏳ ", style="cyan")
|
||||
status_text.append(live_status.current_status or "Processing...", style="dim")
|
||||
|
||||
# Show queued messages if any
|
||||
queue_count = message_queue.count
|
||||
if queue_count > 0:
|
||||
status_text.append(f"\n📋 Queued: {queue_count}", style="dim yellow")
|
||||
|
||||
live.update(Panel(status_text, border_style="dim"))
|
||||
time.sleep(0.1)
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[dim]Processing interrupted[/dim]")
|
||||
# Wait for completion with periodic status updates (non-blocking style)
|
||||
start_time = time.time()
|
||||
last_status_update = ""
|
||||
while not result_container['done']:
|
||||
time.sleep(0.2)
|
||||
current_status = live_status.current_status
|
||||
# Only print status if it changed
|
||||
if current_status != last_status_update:
|
||||
# Use carriage return to update in place
|
||||
elapsed = time.time() - start_time
|
||||
queue_count = message_queue.count
|
||||
queue_info = f" | 📋 Queued: {queue_count}" if queue_count > 0 else ""
|
||||
sys.stdout.write(f"\r[dim cyan]⏳ {current_status} ({elapsed:.1f}s){queue_info}[/dim cyan]")
|
||||
sys.stdout.flush()
|
||||
last_status_update = current_status
|
||||
|
||||
# Clear the status line
|
||||
sys.stdout.write("\r" + " " * 80 + "\r")
|
||||
sys.stdout.flush()
|
||||
|
||||
# Wait for thread to finish
|
||||
bg_thread.join(timeout=1.0)
|
||||
|
||||
# Handle result
|
||||
if result_container['error']:
|
||||
console.print(f"\n[red]Error: {result_container['error']}[/red]")
|
||||
console.print(f"[red]Error: {result_container['error']}[/red]")
|
||||
elif result_container['response']:
|
||||
# Print response with streaming effect
|
||||
console.print()
|
||||
response_str = result_container['response']
|
||||
words = response_str.split()
|
||||
import sys
|
||||
for i, word in enumerate(words):
|
||||
console.print(word + " ", end="")
|
||||
sys.stdout.flush()
|
||||
|
||||
@@ -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.3.75 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.78 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.75"
|
||||
__version__ = "2.3.78"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.1.23",
|
||||
"praisonaiagents>=0.1.24",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
Generated
+4
-4
@@ -4197,7 +4197,7 @@ requires-dist = [
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.1.23" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.1.24" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4252,7 +4252,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4261,9 +4261,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/5e/563a931f0d79f6c64fa8f891ad6eb88cacbbd3f26af5efe52c07a31f11b4/praisonaiagents-0.1.23.tar.gz", hash = "sha256:7a840172892c67feb68e04167db827017d5df8a0e496eb91e309d0e272db60c9", size = 678076, upload-time = "2025-12-22T17:00:28.27Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/31/3e/9dec08dd8107b963cdb3ea3a77b5530e7f68b239c38c80f713be27920f1b/praisonaiagents-0.1.24.tar.gz", hash = "sha256:8a6d17c2df22f2171df11b44b44b817e72a6f34f1f5b4e389691bedbbff06621", size = 678110, upload-time = "2025-12-23T05:39:18.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/b3/bf02847078595ab1cf109dfd3c7282eaece21f7b879fe96a3ae0d23e8571/praisonaiagents-0.1.23-py3-none-any.whl", hash = "sha256:a455c8fd04fdbd7dd7bf9755c24f19c9c496c404ef60a4fdfc4d4a51decb2e60", size = 616785, upload-time = "2025-12-22T17:00:26.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/33/3b45e341433a01408f59ced6eb39da3ecb17067f0ff3c954fa38e07aaffb/praisonaiagents-0.1.24-py3-none-any.whl", hash = "sha256:03a1c476bb5287ad9e519b85a17a93c42ce11b01db87009fcde17a1901e4828a", size = 616792, upload-time = "2025-12-23T05:39:16.12Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user