mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 11:51:37 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85ae215025 | ||
|
|
056e4f5baa | ||
|
|
8f06d18e5a | ||
|
|
f8c6f32d00 |
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>=4.5.79" \
|
||||
"praisonai>=4.5.80" \
|
||||
"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>=4.5.79" \
|
||||
"praisonai>=4.5.80" \
|
||||
"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>=4.5.79" \
|
||||
"praisonai>=4.5.80" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -23,12 +23,22 @@ def main():
|
||||
@register_tool("custom.calculate")
|
||||
def calculate(expression: str) -> str:
|
||||
"""Safely evaluate a mathematical expression."""
|
||||
import ast
|
||||
import operator
|
||||
try:
|
||||
# Only allow safe math operations
|
||||
allowed = set("0123456789+-*/.(). ")
|
||||
if not all(c in allowed for c in expression):
|
||||
return "Error: Invalid characters in expression"
|
||||
result = eval(expression) # Safe due to character filtering
|
||||
# Use ast.parse for safe evaluation of math expressions
|
||||
tree = ast.parse(expression, mode='eval')
|
||||
# Only allow numeric literals and basic math operators
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, (ast.Expression, ast.BinOp, ast.UnaryOp,
|
||||
ast.Constant, ast.Add, ast.Sub, ast.Mult,
|
||||
ast.Div, ast.USub, ast.UAdd)):
|
||||
return "Error: Only basic math operations are allowed"
|
||||
result = eval(compile(tree, '<expr>', 'eval'), {"__builtins__": {}})
|
||||
return f"Result: {result}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
@@ -44,12 +44,20 @@ def main():
|
||||
|
||||
def calculate(expression: str) -> str:
|
||||
"""Calculate a math expression safely."""
|
||||
import ast
|
||||
try:
|
||||
# Safe eval for basic math
|
||||
allowed = set("0123456789+-*/.() ")
|
||||
if all(c in allowed for c in expression):
|
||||
return str(eval(expression))
|
||||
return "Error: Invalid expression"
|
||||
if not all(c in allowed for c in expression):
|
||||
return "Error: Invalid expression"
|
||||
# Use ast.parse to validate expression safety
|
||||
tree = ast.parse(expression, mode='eval')
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, (ast.Expression, ast.BinOp, ast.UnaryOp,
|
||||
ast.Constant, ast.Add, ast.Sub, ast.Mult,
|
||||
ast.Div, ast.USub, ast.UAdd)):
|
||||
return "Error: Only basic math operations are allowed"
|
||||
return str(eval(compile(tree, '<expr>', 'eval'), {"__builtins__": {}}))
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
@@ -135,9 +135,30 @@ def main():
|
||||
return {"error": str(e)}
|
||||
|
||||
# MCP endpoints
|
||||
def safe_calculate(expression: str) -> str:
|
||||
"""Safely evaluate a mathematical expression using AST."""
|
||||
import ast
|
||||
try:
|
||||
# Only allow safe math characters
|
||||
allowed = set("0123456789+-*/.() ")
|
||||
if not all(c in allowed for c in expression):
|
||||
return "Error: Invalid characters in expression"
|
||||
# Use ast.parse for safe evaluation
|
||||
tree = ast.parse(expression, mode='eval')
|
||||
# Only allow numeric literals and basic math operators
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, (ast.Expression, ast.BinOp, ast.UnaryOp,
|
||||
ast.Constant, ast.Add, ast.Sub, ast.Mult,
|
||||
ast.Div, ast.USub, ast.UAdd)):
|
||||
return "Error: Only basic math operations are allowed"
|
||||
result = eval(compile(tree, '<expr>', 'eval'), {"__builtins__": {}})
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
mcp_tools = {
|
||||
"search": {"description": "Search the web", "func": lambda q: f"Results for: {q}"},
|
||||
"calculate": {"description": "Calculate expression", "func": lambda e: str(eval(e)) if e.replace(" ", "").replace("+", "").replace("-", "").replace("*", "").replace("/", "").replace(".", "").isdigit() or True else "Error"},
|
||||
"calculate": {"description": "Calculate expression", "func": safe_calculate},
|
||||
}
|
||||
|
||||
@app.get("/mcp/tools")
|
||||
|
||||
@@ -93,4 +93,5 @@ chroma_db/
|
||||
|
||||
.praisonai
|
||||
.windsurf
|
||||
.agent
|
||||
.agent
|
||||
.sfdx
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
from praisonaiagents import Agent, Task, AgentTeam
|
||||
from praisonaiagents.memory import Memory
|
||||
from praisonaiagents.agent import ContextAgent
|
||||
@@ -10,7 +12,7 @@ context_output = context_agent.start("https://github.com/MervinPraison/PraisonAI
|
||||
mongodb_memory_config = {
|
||||
"provider": "mongodb",
|
||||
"config": {
|
||||
"connection_string": "mongodb+srv://username:[email protected]/?retryWrites=true&w=majority&appName=Cluster2",
|
||||
"connection_string": os.environ.get("MONGODB_CONNECTION_STRING", "mongodb+srv://localhost:27017/?retryWrites=true&w=majority"),
|
||||
"database": "praisonai_memory",
|
||||
"use_vector_search": True,
|
||||
"max_pool_size": 50,
|
||||
@@ -49,7 +51,7 @@ print("\n=== MEMORY VALIDATION ===")
|
||||
|
||||
try:
|
||||
# Check MongoDB connection and collections
|
||||
client = pymongo.MongoClient("mongodb+srv://username:[email protected]/?retryWrites=true&w=majority&appName=Cluster2")
|
||||
client = pymongo.MongoClient(os.environ.get("MONGODB_CONNECTION_STRING", "mongodb+srv://localhost:27017/?retryWrites=true&w=majority"))
|
||||
db = client["praisonai_memory"]
|
||||
collections = db.list_collection_names()
|
||||
print(f"Available collections: {collections}")
|
||||
|
||||
@@ -42,26 +42,23 @@ def _check_crawl4ai_available() -> tuple[bool, Optional[str]]:
|
||||
|
||||
# Check that Playwright browser binaries have been downloaded.
|
||||
# `playwright install` is a post-install OS step that pip never runs.
|
||||
# We check the standard ms-playwright cache directory directly — no
|
||||
# API calls, no process launches, no private internals.
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright # noqa: F401 — import-only check
|
||||
import subprocess, sys
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "playwright", "install", "--dry-run"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
# If dry-run output mentions 'chromium' as needing install, browsers are missing
|
||||
if "chromium" in result.stdout.lower() and "download" in result.stdout.lower():
|
||||
import os, pathlib
|
||||
ms_pw = pathlib.Path(os.environ.get(
|
||||
"PLAYWRIGHT_BROWSERS_PATH",
|
||||
pathlib.Path.home() / ".cache" / "ms-playwright"
|
||||
))
|
||||
if ms_pw.exists() and not list(ms_pw.glob("chromium-*")):
|
||||
return False, (
|
||||
"Playwright browsers are not installed.\n"
|
||||
"Run this command once to download them:\n"
|
||||
" playwright install chromium\n"
|
||||
"Or for Docker/CI: playwright install chromium --with-deps"
|
||||
)
|
||||
except FileNotFoundError:
|
||||
# playwright binary not found at all — package missing
|
||||
pass
|
||||
# If ms_pw doesn't exist yet, skip — _get_crawler will catch it.
|
||||
except Exception:
|
||||
# dry-run failed for another reason; proceed and let _get_crawler catch it
|
||||
pass
|
||||
|
||||
return True, None
|
||||
@@ -612,7 +609,7 @@ def crawl4ai_sync(
|
||||
Returns:
|
||||
Dict with crawl results
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
return asyncio.run(
|
||||
crawl4ai(url, css_selector, js_code, wait_for)
|
||||
)
|
||||
|
||||
@@ -632,7 +629,7 @@ def crawl4ai_extract_sync(
|
||||
Returns:
|
||||
Dict with extracted data
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
return asyncio.run(
|
||||
crawl4ai_extract(url, schema, js_code)
|
||||
)
|
||||
|
||||
|
||||
@@ -286,12 +286,26 @@ class FileTools:
|
||||
}
|
||||
|
||||
try:
|
||||
# Validate URL to prevent SSRF
|
||||
from urllib.parse import urlparse
|
||||
parsed_url = urlparse(url)
|
||||
if parsed_url.scheme not in ("http", "https"):
|
||||
return {"success": False, "path": "", "size": 0, "error": f"Unsupported URL scheme: {parsed_url.scheme}"}
|
||||
import ipaddress, socket
|
||||
try:
|
||||
hostname = parsed_url.hostname or ""
|
||||
ip = ipaddress.ip_address(socket.gethostbyname(hostname))
|
||||
if ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local:
|
||||
return {"success": False, "path": "", "size": 0, "error": f"URL resolves to private/reserved IP: {ip}"}
|
||||
except (socket.gaierror, ValueError):
|
||||
pass # Let the request handle DNS resolution errors
|
||||
|
||||
# Validate destination path
|
||||
safe_path = FileTools._validate_path(destination)
|
||||
|
||||
|
||||
# Create directory if needed
|
||||
os.makedirs(os.path.dirname(safe_path) or ".", exist_ok=True)
|
||||
|
||||
|
||||
# Download file
|
||||
with httpx.stream("GET", url, timeout=timeout, follow_redirects=True) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.79"
|
||||
version = "1.5.80"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+1
-1
@@ -2951,7 +2951,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.79"
|
||||
version = "1.5.80"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
+20
-1
@@ -1,4 +1,5 @@
|
||||
from flask import Flask
|
||||
from markupsafe import escape
|
||||
import markdown
|
||||
|
||||
app = Flask(__name__)
|
||||
@@ -11,7 +12,25 @@ def basic():
|
||||
@app.route('/')
|
||||
def home():
|
||||
output = basic()
|
||||
html_output = markdown.markdown(output)
|
||||
# Convert markdown to HTML, then sanitize the output to prevent XSS
|
||||
html_output = markdown.markdown(str(output))
|
||||
# Use bleach to sanitize HTML output (allows safe tags only)
|
||||
try:
|
||||
import bleach
|
||||
allowed_tags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li',
|
||||
'strong', 'em', 'code', 'pre', 'blockquote', 'a', 'br', 'hr']
|
||||
allowed_attrs = {'a': ['href', 'title']}
|
||||
# Strip javascript: URLs from href attributes
|
||||
def filter_href(tag, name, value):
|
||||
if name == 'href':
|
||||
if value.lower().startswith(('javascript:', 'data:', 'vbscript:')):
|
||||
return False
|
||||
return True
|
||||
html_output = bleach.clean(html_output, tags=allowed_tags, attributes=allowed_attrs,
|
||||
strip=True, protocols=['http', 'https', 'mailto'])
|
||||
except ImportError:
|
||||
# Fallback: escape the entire output if bleach not available
|
||||
html_output = str(escape(html_output))
|
||||
return f'<html><body>{html_output}</body></html>'
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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/v4.5.79.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.79.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.80.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v4.5.80.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -271,6 +271,23 @@ class URLReader:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Validate URL to prevent SSRF
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(source)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
logger.error(f"Unsupported URL scheme: {parsed.scheme}")
|
||||
return []
|
||||
import ipaddress
|
||||
try:
|
||||
import socket
|
||||
hostname = parsed.hostname or ""
|
||||
ip = ipaddress.ip_address(socket.gethostbyname(hostname))
|
||||
if ip.is_private or ip.is_loopback or ip.is_reserved or ip.is_link_local:
|
||||
logger.error(f"URL resolves to private/reserved IP: {ip}")
|
||||
return []
|
||||
except (socket.gaierror, ValueError):
|
||||
pass # Let the request handle DNS resolution errors
|
||||
|
||||
response = httpx.get(source, timeout=30, follow_redirects=True)
|
||||
response.raise_for_status()
|
||||
content = response.text
|
||||
|
||||
@@ -312,13 +312,23 @@ class HybridWorkflowExecutor:
|
||||
return flags
|
||||
|
||||
def _eval_condition(self, condition: str, flags: Dict) -> bool:
|
||||
"""Evaluate a simple condition expression."""
|
||||
"""Evaluate a simple condition expression safely."""
|
||||
import ast
|
||||
|
||||
condition = condition.strip()
|
||||
if condition.startswith("{{") and condition.endswith("}}"):
|
||||
condition = condition[2:-2].strip()
|
||||
|
||||
context = {"flags": type("Flags", (), flags)(), "env": os.environ}
|
||||
# Handle simple attribute lookups like "flags.verbose" or "env.DEBUG"
|
||||
context = {"flags": flags, "env": dict(os.environ)}
|
||||
try:
|
||||
return bool(eval(condition, {"__builtins__": {}}, context))
|
||||
except Exception:
|
||||
# Only allow simple attribute access patterns: obj.attr
|
||||
parts = condition.split(".")
|
||||
if len(parts) == 2 and parts[0] in context:
|
||||
obj = context[parts[0]]
|
||||
if isinstance(obj, dict):
|
||||
return bool(obj.get(parts[1], False))
|
||||
# Try literal eval for simple boolean/numeric expressions
|
||||
return bool(ast.literal_eval(condition))
|
||||
except (ValueError, KeyError, TypeError):
|
||||
return False
|
||||
@@ -719,17 +719,26 @@ class JobWorkflowExecutor:
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _eval_condition(self, condition: str, flags: Dict) -> bool:
|
||||
"""Evaluate a simple condition expression."""
|
||||
"""Evaluate a simple condition expression safely."""
|
||||
import ast
|
||||
|
||||
# Strip {{ }} if present
|
||||
condition = condition.strip()
|
||||
if condition.startswith("{{") and condition.endswith("}}"):
|
||||
condition = condition[2:-2].strip()
|
||||
|
||||
# Build evaluation context
|
||||
context = {"flags": type("Flags", (), flags)(), "env": os.environ}
|
||||
# Handle simple attribute lookups like "flags.verbose" or "env.DEBUG"
|
||||
context = {"flags": flags, "env": dict(os.environ)}
|
||||
try:
|
||||
return bool(eval(condition, {"__builtins__": {}}, context))
|
||||
except Exception:
|
||||
# Only allow simple attribute access patterns: obj.attr
|
||||
parts = condition.split(".")
|
||||
if len(parts) == 2 and parts[0] in context:
|
||||
obj = context[parts[0]]
|
||||
if isinstance(obj, dict):
|
||||
return bool(obj.get(parts[1], False))
|
||||
# Try literal eval for simple boolean/numeric expressions
|
||||
return bool(ast.literal_eval(condition))
|
||||
except (ValueError, KeyError, TypeError):
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -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==4.5.79 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==4.5.80 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ class SurrealDBConversationStore(ConversationStore):
|
||||
params["after"] = after
|
||||
|
||||
where = "WHERE " + " AND ".join(conditions)
|
||||
limit_clause = f"LIMIT {limit}" if limit else ""
|
||||
limit_clause = f"LIMIT {int(limit)}" if limit else ""
|
||||
|
||||
result = self._run_sync(
|
||||
self._client.query(
|
||||
|
||||
@@ -6,6 +6,7 @@ Install: pip install clickhouse-connect
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .base import KnowledgeStore, KnowledgeDocument
|
||||
@@ -13,6 +14,13 @@ from .base import KnowledgeStore, KnowledgeDocument
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _sanitize_identifier(name: str) -> str:
|
||||
"""Sanitize a SQL identifier (table/database name) to prevent injection."""
|
||||
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_.]*$', name):
|
||||
raise ValueError(f"Invalid identifier: {name}")
|
||||
return name
|
||||
|
||||
|
||||
class ClickHouseKnowledgeStore(KnowledgeStore):
|
||||
"""
|
||||
ClickHouse-based knowledge store using vector search.
|
||||
@@ -52,11 +60,11 @@ class ClickHouseKnowledgeStore(KnowledgeStore):
|
||||
self.database = database
|
||||
|
||||
# Create database if not exists
|
||||
self._client.command(f"CREATE DATABASE IF NOT EXISTS {database}")
|
||||
self._client.command(f"CREATE DATABASE IF NOT EXISTS {_sanitize_identifier(database)}")
|
||||
logger.info(f"Connected to ClickHouse database: {database}")
|
||||
|
||||
def _table_name(self, collection: str) -> str:
|
||||
return f"{self.database}.praison_vec_{collection}"
|
||||
return _sanitize_identifier(f"{self.database}.praison_vec_{collection}")
|
||||
|
||||
def create_collection(
|
||||
self,
|
||||
@@ -98,10 +106,11 @@ class ClickHouseKnowledgeStore(KnowledgeStore):
|
||||
|
||||
def list_collections(self) -> List[str]:
|
||||
"""List all collections."""
|
||||
result = self._client.query(f"""
|
||||
SELECT name FROM system.tables
|
||||
WHERE database = '{self.database}' AND name LIKE 'praison_vec_%'
|
||||
""")
|
||||
db = _sanitize_identifier(self.database)
|
||||
result = self._client.query(
|
||||
"SELECT name FROM system.tables WHERE database = %(db)s AND name LIKE 'praison_vec_%%'",
|
||||
parameters={"db": db}
|
||||
)
|
||||
return [row[0].replace("praison_vec_", "") for row in result.result_rows]
|
||||
|
||||
def insert(
|
||||
@@ -156,19 +165,20 @@ class ClickHouseKnowledgeStore(KnowledgeStore):
|
||||
"""Search for similar documents using cosine distance."""
|
||||
table = self._table_name(collection)
|
||||
|
||||
# Convert embedding to string for query
|
||||
embedding_str = "[" + ",".join(map(str, query_embedding)) + "]"
|
||||
|
||||
# Convert embedding to validated float array for query
|
||||
embedding_str = "[" + ",".join(str(float(x)) for x in query_embedding) + "]"
|
||||
limit = int(limit)
|
||||
|
||||
query = f"""
|
||||
SELECT
|
||||
SELECT
|
||||
id, content, content_hash, created_at,
|
||||
1 - cosineDistance(embedding, {embedding_str}) as score
|
||||
FROM {table}
|
||||
"""
|
||||
|
||||
|
||||
if score_threshold:
|
||||
query += f" WHERE 1 - cosineDistance(embedding, {embedding_str}) >= {score_threshold}"
|
||||
|
||||
query += f" WHERE 1 - cosineDistance(embedding, {embedding_str}) >= {float(score_threshold)}"
|
||||
|
||||
query += f" ORDER BY score DESC LIMIT {limit}"
|
||||
|
||||
result = self._client.query(query)
|
||||
@@ -195,11 +205,12 @@ class ClickHouseKnowledgeStore(KnowledgeStore):
|
||||
"""Get documents by IDs."""
|
||||
table = self._table_name(collection)
|
||||
|
||||
id_list = ", ".join([f"'{i}'" for i in ids])
|
||||
result = self._client.query(f"""
|
||||
SELECT id, content, content_hash, created_at, embedding
|
||||
FROM {table} WHERE id IN ({id_list})
|
||||
""")
|
||||
placeholders = ", ".join(["%(id_{})s".format(idx) for idx in range(len(ids))])
|
||||
params = {"id_{}".format(idx): i for idx, i in enumerate(ids)}
|
||||
result = self._client.query(
|
||||
f"SELECT id, content, content_hash, created_at, embedding FROM {table} WHERE id IN ({placeholders})",
|
||||
parameters=params
|
||||
)
|
||||
|
||||
documents = []
|
||||
for row in result.result_rows:
|
||||
@@ -225,8 +236,12 @@ class ClickHouseKnowledgeStore(KnowledgeStore):
|
||||
table = self._table_name(collection)
|
||||
|
||||
if ids:
|
||||
id_list = ", ".join([f"'{i}'" for i in ids])
|
||||
self._client.command(f"ALTER TABLE {table} DELETE WHERE id IN ({id_list})")
|
||||
placeholders = ", ".join(["%(id_{})s".format(idx) for idx in range(len(ids))])
|
||||
params = {"id_{}".format(idx): i for idx, i in enumerate(ids)}
|
||||
self._client.command(
|
||||
f"ALTER TABLE {table} DELETE WHERE id IN ({placeholders})",
|
||||
parameters=params
|
||||
)
|
||||
return len(ids)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Install: pip install lancedb
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .base import KnowledgeStore, KnowledgeDocument
|
||||
@@ -13,6 +14,18 @@ from .base import KnowledgeStore, KnowledgeDocument
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _sanitize_identifier(name: str) -> str:
|
||||
"""Sanitize a SQL identifier to prevent injection."""
|
||||
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name):
|
||||
raise ValueError(f"Invalid identifier: {name}")
|
||||
return name
|
||||
|
||||
|
||||
def _escape_value(val: str) -> str:
|
||||
"""Escape a string value for use in LanceDB WHERE clauses."""
|
||||
return str(val).replace("'", "''").replace("\\", "\\\\")
|
||||
|
||||
|
||||
class LanceDBKnowledgeStore(KnowledgeStore):
|
||||
"""
|
||||
LanceDB-based knowledge store for vector search.
|
||||
@@ -140,9 +153,9 @@ class LanceDBKnowledgeStore(KnowledgeStore):
|
||||
query = table.search(query_embedding).limit(limit)
|
||||
|
||||
if filters:
|
||||
where_clauses = [f"{k} = '{v}'" for k, v in filters.items()]
|
||||
where_clauses = [f"{_sanitize_identifier(k)} = '{_escape_value(v)}'" for k, v in filters.items()]
|
||||
query = query.where(" AND ".join(where_clauses))
|
||||
|
||||
|
||||
results = query.to_pandas()
|
||||
|
||||
documents = []
|
||||
@@ -170,7 +183,7 @@ class LanceDBKnowledgeStore(KnowledgeStore):
|
||||
"""Get documents by IDs."""
|
||||
table = self._get_table(collection)
|
||||
|
||||
id_list = ", ".join([f"'{i}'" for i in ids])
|
||||
id_list = ", ".join([f"'{_escape_value(i)}'" for i in ids])
|
||||
results = table.search().where(f"id IN ({id_list})").to_pandas()
|
||||
|
||||
documents = []
|
||||
@@ -197,11 +210,11 @@ class LanceDBKnowledgeStore(KnowledgeStore):
|
||||
table = self._get_table(collection)
|
||||
|
||||
if ids:
|
||||
id_list = ", ".join([f"'{i}'" for i in ids])
|
||||
id_list = ", ".join([f"'{_escape_value(i)}'" for i in ids])
|
||||
table.delete(f"id IN ({id_list})")
|
||||
return len(ids)
|
||||
elif filters:
|
||||
where_clauses = [f"{k} = '{v}'" for k, v in filters.items()]
|
||||
where_clauses = [f"{_sanitize_identifier(k)} = '{_escape_value(v)}'" for k, v in filters.items()]
|
||||
table.delete(" AND ".join(where_clauses))
|
||||
return -1
|
||||
return 0
|
||||
|
||||
@@ -6,6 +6,7 @@ Install: pip install pymilvus
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from .base import KnowledgeStore, KnowledgeDocument
|
||||
@@ -13,6 +14,18 @@ from .base import KnowledgeStore, KnowledgeDocument
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _sanitize_identifier(name: str) -> str:
|
||||
"""Sanitize a Milvus field name to prevent injection."""
|
||||
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name):
|
||||
raise ValueError(f"Invalid field name: {name}")
|
||||
return name
|
||||
|
||||
|
||||
def _escape_value(val: str) -> str:
|
||||
"""Escape a string value for use in Milvus expressions."""
|
||||
return str(val).replace('\\', '\\\\').replace('"', '\\"')
|
||||
|
||||
|
||||
class MilvusKnowledgeStore(KnowledgeStore):
|
||||
"""
|
||||
Milvus-based knowledge store for vector search.
|
||||
@@ -180,9 +193,9 @@ class MilvusKnowledgeStore(KnowledgeStore):
|
||||
|
||||
expr = None
|
||||
if filters:
|
||||
conditions = [f'{k} == "{v}"' for k, v in filters.items()]
|
||||
conditions = [f'{_sanitize_identifier(k)} == "{_escape_value(v)}"' for k, v in filters.items()]
|
||||
expr = " and ".join(conditions)
|
||||
|
||||
|
||||
results = col.search(
|
||||
data=[query_embedding],
|
||||
anns_field="vector",
|
||||
@@ -220,9 +233,9 @@ class MilvusKnowledgeStore(KnowledgeStore):
|
||||
col = Collection(collection)
|
||||
col.load()
|
||||
|
||||
id_list = ", ".join([f'"{i}"' for i in ids])
|
||||
id_list = ", ".join([f'"{_escape_value(i)}"' for i in ids])
|
||||
expr = f"id in [{id_list}]"
|
||||
|
||||
|
||||
results = col.query(
|
||||
expr=expr,
|
||||
output_fields=["id", "content", "content_hash", "created_at", "vector"],
|
||||
@@ -254,12 +267,12 @@ class MilvusKnowledgeStore(KnowledgeStore):
|
||||
col.load()
|
||||
|
||||
if ids:
|
||||
id_list = ", ".join([f'"{i}"' for i in ids])
|
||||
id_list = ", ".join([f'"{_escape_value(i)}"' for i in ids])
|
||||
expr = f"id in [{id_list}]"
|
||||
col.delete(expr)
|
||||
return len(ids)
|
||||
elif filters:
|
||||
conditions = [f'{k} == "{v}"' for k, v in filters.items()]
|
||||
conditions = [f'{_sanitize_identifier(k)} == "{_escape_value(v)}"' for k, v in filters.items()]
|
||||
expr = " and ".join(conditions)
|
||||
col.delete(expr)
|
||||
return -1
|
||||
|
||||
@@ -5,6 +5,7 @@ Requires: singlestoredb
|
||||
Install: pip install singlestoredb
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -227,7 +228,7 @@ class SingleStoreVectorKnowledgeStore(KnowledgeStore):
|
||||
documents.append(KnowledgeDocument(
|
||||
id=row[0],
|
||||
content=row[1],
|
||||
metadata=eval(row[2]) if row[2] else None,
|
||||
metadata=json.loads(row[2]) if row[2] else None,
|
||||
content_hash=row[3],
|
||||
created_at=row[4]
|
||||
))
|
||||
@@ -256,7 +257,7 @@ class SingleStoreVectorKnowledgeStore(KnowledgeStore):
|
||||
KnowledgeDocument(
|
||||
id=row[0],
|
||||
content=row[1],
|
||||
metadata=eval(row[2]) if row[2] else None,
|
||||
metadata=json.loads(row[2]) if row[2] else None,
|
||||
content_hash=row[3],
|
||||
created_at=row[4]
|
||||
)
|
||||
|
||||
@@ -199,8 +199,9 @@ class SurrealDBVectorKnowledgeStore(KnowledgeStore):
|
||||
"""Search for similar documents using vector search."""
|
||||
self._init_client()
|
||||
|
||||
# SurrealDB vector search query
|
||||
embedding_str = str(query_embedding)
|
||||
# SurrealDB vector search query - validate numeric inputs
|
||||
embedding_str = "[" + ",".join(str(float(x)) for x in query_embedding) + "]"
|
||||
limit = int(limit)
|
||||
query = f"""
|
||||
SELECT *, vector::similarity::cosine(embedding, {embedding_str}) AS score
|
||||
FROM {collection}
|
||||
|
||||
@@ -56,7 +56,7 @@ class PlanApplier:
|
||||
# Try to use ruamel.yaml for comment preservation
|
||||
try:
|
||||
from ruamel.yaml import YAML
|
||||
self._yaml = YAML()
|
||||
self._yaml = YAML(typ='safe')
|
||||
self._yaml.preserve_quotes = True
|
||||
self._use_ruamel = True
|
||||
except ImportError:
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.5.79"
|
||||
__version__ = "4.5.80"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=1.5.79",
|
||||
"praisonaiagents>=1.5.80",
|
||||
"python-dotenv>=0.19.0",
|
||||
"litellm>=1.81.0",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
Generated
+4
-4
@@ -4939,7 +4939,7 @@ requires-dist = [
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.1.0" },
|
||||
{ name = "praisonaiagents", specifier = ">=1.5.79" },
|
||||
{ name = "praisonaiagents", specifier = ">=1.5.80" },
|
||||
{ name = "praisonaiagents", extras = ["all"], marker = "extra == 'claw'", specifier = ">=1.5.40" },
|
||||
{ name = "psutil", marker = "extra == 'all'", specifier = ">=5.9.0" },
|
||||
{ name = "psutil", marker = "extra == 'bot'", specifier = ">=5.9.0" },
|
||||
@@ -5009,7 +5009,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "1.5.79"
|
||||
version = "1.5.80"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -5018,9 +5018,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/ab/11208adb22f9c7c8ce5bf3b8434818c253cdfc25bdfaa226d534d0977021/praisonaiagents-1.5.79.tar.gz", hash = "sha256:2cc31088ca1cdab5af32d0caee02e850bd9b8c238e5f5a3b8669824b123a543b", size = 1204517, upload-time = "2026-03-18T15:55:33.396Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/3b/6c88188227bac16f88e7b141568cebb03d6ef46f38935e3bb8408a400017/praisonaiagents-1.5.80.tar.gz", hash = "sha256:fa0b803943e5aab8ce4099c8257e8f5378648a19668ec046749aa3babad99d57", size = 1204298, upload-time = "2026-03-18T21:06:55.71Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f6/f8c376a0f76c61c000a5ecc96e6ffbf8277c301101c5fc4219120ed38a88/praisonaiagents-1.5.79-py3-none-any.whl", hash = "sha256:60edaa167bfe4d06c039012e2ef2ad63f0aa5cd4cab753fee0a00867f701c313", size = 1248925, upload-time = "2026-03-18T15:55:31.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/76/ab37e313ad3e756c39df8f5b180613169ff8074a293aaba52fbdef88928e/praisonaiagents-1.5.80-py3-none-any.whl", hash = "sha256:e1590e6bbbda526e13d3654ab17cab6a2a602af4508fabe7a8cd389213658efe", size = 1248857, upload-time = "2026-03-18T21:06:53.898Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
Reference in new issue
Block a user