tools / tools.py
ayb-bh92's picture
Update tools.py
a60a821 verified
Raw History Blame Contribute Delete
41.4 kB
# ุฌู…ูŠุน ุงู„ุฃุฏูˆุงุช ุงู„ู…ุญู„ูŠุฉ ู…ุน ุฏุนู… ุงู„ู„ุบุฉ ุงู„ุนุฑุจูŠุฉ
import os
import io
import json
import base64
import uuid
import asyncio
import httpx
from typing import Dict, Any, Callable, Optional, List
from dataclasses import dataclass, field
from pathlib import Path
from fastapi import Request
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ุงุณุชูŠุฑุงุฏุงุช ReportLab
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_RIGHT
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ุฏุนู… ุงู„ู„ุบุฉ ุงู„ุนุฑุจูŠุฉ ููŠ ู…ู„ูุงุช PDF
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
BASE_DIR = Path(__file__).parent
FONTS_DIR = BASE_DIR / "fonts"
FONTS_DIR.mkdir(exist_ok=True)
ARABIC_FONT_PATH = FONTS_DIR / "Amiri-Regular.ttf"
ARABIC_FONT_URL = "https://raw.githubusercontent.com/google/fonts/main/ofl/amiri/Amiri-Regular.ttf"
ARABIC_FONT_NAME = "Amiri"
_arabic_font_ready = False
async def _ensure_arabic_font() -> None:
"""ุชุฃูƒุฏ ู…ู† ูˆุฌูˆุฏ ุงู„ุฎุท ุงู„ุนุฑุจูŠุŒ ูˆุญู…ู„ู‡ ุฅุฐุง ู„ู… ูŠูƒู† ู…ูˆุฌูˆุฏุงู‹"""
global _arabic_font_ready
try:
if not ARABIC_FONT_PATH.exists():
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
r = await client.get(ARABIC_FONT_URL)
r.raise_for_status()
ARABIC_FONT_PATH.write_bytes(r.content)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
pdfmetrics.registerFont(TTFont(ARABIC_FONT_NAME, str(ARABIC_FONT_PATH)))
_arabic_font_ready = True
print("โœ… ุชู… ุชุฌู‡ูŠุฒ ุงู„ุฎุท ุงู„ุนุฑุจูŠ ุจู†ุฌุงุญ")
except Exception as e:
print(f"โš ๏ธ ุชุนุฐู‘ุฑ ุชุฌู‡ูŠุฒ ุงู„ุฎุท ุงู„ุนุฑุจูŠ: {e}")
_arabic_font_ready = False
def _shape_text(text: str) -> str:
"""ุฅุนุงุฏุฉ ุชุดูƒูŠู„ ุงู„ู†ุต ุงู„ุนุฑุจูŠ ู„ู„ุนุฑุถ ุงู„ุตุญูŠุญ ููŠ PDF"""
try:
import arabic_reshaper
from bidi.algorithm import get_display
if not text:
return text
reshaped = arabic_reshaper.reshape(text)
return get_display(reshaped)
except Exception as e:
print(f"โš ๏ธ ูุดู„ ุชุดูƒูŠู„ ุงู„ู†ุต ุงู„ุนุฑุจูŠ: {e}")
return text
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 1. ุชุนุฑูŠู ุจู†ูŠุฉ ุงู„ุฃุฏุงุฉ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@dataclass
class LocalTool:
"""ุชู…ุซูŠู„ ูˆุตููŠ ู„ู„ุฃุฏุงุฉ"""
name: str
description: str
parameters: Dict[str, Any]
callable: Callable
category: str = "General"
aliases: List[str] = field(default_factory=list)
tags: List[str] = field(default_factory=list)
enabled: bool = True
hidden: bool = False
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 2. Decorator ู„ู„ุชุณุฌูŠู„
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
TOOLS: Dict[str, LocalTool] = {}
def tool(
*,
name: Optional[str] = None,
description: str,
parameters: Dict[str, Any],
category: str = "General",
aliases: Optional[List[str]] = None,
tags: Optional[List[str]] = None,
enabled: bool = True,
hidden: bool = False
) -> Callable:
"""Decorator ู„ุชุณุฌูŠู„ ุงู„ุฃุฏูˆุงุช ุงู„ู…ุญู„ูŠุฉ"""
def wrapper(fn: Callable) -> Callable:
tool_name = name or fn.__name__
TOOLS[tool_name] = LocalTool(
name=tool_name,
description=description,
parameters=parameters,
callable=fn,
category=category,
aliases=aliases or [],
tags=tags or [],
enabled=enabled,
hidden=hidden
)
return fn
return wrapper
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 3. ุฏูˆุงู„ ู…ุณุงุนุฏุฉ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def _apply_markdown_lines(add_heading, add_paragraph, add_bullet, text: str):
for raw in text.split("\n"):
line = raw.strip()
if not line:
continue
if line.startswith("### "):
add_heading(line[4:], 3)
elif line.startswith("## "):
add_heading(line[3:], 2)
elif line.startswith("# "):
add_heading(line[2:], 1)
elif line.startswith(("- ", "* ")):
add_bullet(line[2:])
else:
add_paragraph(line)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 4. ุฃุฏูˆุงุช Firecrawl โ€” ู…ูุตุญูŽู‘ุญุฉ โœ…
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@tool(
name="firecrawl_search",
description="ุจุญุซ ููŠ ุงู„ูˆูŠุจ ุนุจุฑ Firecrawl ูˆุฅุฑุฌุงุน ุฃู‡ู… ุงู„ู†ุชุงุฆุฌ.",
parameters={
"type": "object",
"properties": {
"query": {"type": "string", "description": "ู†ุต ุงู„ุจุญุซ"},
"limit": {"type": "integer", "description": "ุนุฏุฏ ุงู„ู†ุชุงุฆุฌ", "default": 5}
},
"required": ["query"]
},
category="Search",
aliases=["web_search", "search"],
tags=["search", "web"]
)
async def firecrawl_search(query: str, limit: int = 5, api_key: Optional[str] = None) -> dict:
"""ุจุญุซ ููŠ ุงู„ูˆูŠุจ ุนุจุฑ Firecrawl"""
if not api_key:
return {"error": "FIRECRAWL_API_KEY ุบูŠุฑ ู…ุถุจูˆุท"}
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
r = await client.post(
"https://api.firecrawl.dev/v1/search",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
},
json={"query": query, "limit": limit},
)
if r.status_code != 200:
try:
err_data = r.json()
err_msg = err_data.get("error", str(err_data))
except Exception:
err_msg = r.text[:500]
return {"error": f"Firecrawl search error ({r.status_code}): {err_msg}"}
data = r.json()
results = data.get("data") or data.get("results") or []
out = []
for item in results[:limit]:
out.append({
"title": item.get("title"),
"url": item.get("url"),
"snippet": item.get("description") or (item.get("markdown") or "")[:300],
})
return {"results": out}
@tool(
name="firecrawl_scrape",
description="ุชุตูุญ ุฑุงุจุท ูˆูŠุจ ู…ุญุฏุฏ ูˆุฅุฑุฌุงุน ู…ุญุชูˆุงู‡ ูƒู€ Markdown ู†ุธูŠู.",
parameters={
"type": "object",
"properties": {
"url": {"type": "string", "description": "ุงู„ุฑุงุจุท ุงู„ู…ุทู„ูˆุจ ุชุตูุญู‡"}
},
"required": ["url"]
},
category="Search",
aliases=["scrape", "extract"],
tags=["search", "web"]
)
async def firecrawl_scrape(url: str, api_key: Optional[str] = None) -> dict:
"""ุชุตูุญ ูˆูŠุจ"""
if not api_key:
return {"error": "FIRECRAWL_API_KEY ุบูŠุฑ ู…ุถุจูˆุท"}
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
r = await client.post(
"https://api.firecrawl.dev/v1/scrape",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
},
json={"url": url, "formats": ["markdown"], "onlyMainContent": True},
)
if r.status_code != 200:
try:
err_data = r.json()
err_msg = err_data.get("error", str(err_data))
except Exception:
err_msg = r.text[:500]
return {"error": f"Firecrawl scrape error ({r.status_code}): {err_msg}"}
data = r.json()
markdown = (data.get("data") or {}).get("markdown", "")
return {"markdown": markdown[:8000]}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 5. ุฃุฏูˆุงุช Context7 โ€” ู…ูุตุญูŽู‘ุญุฉ โœ…
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@tool(
name="context7_resolve_library",
description="ุงู„ุจุญุซ ุนู† ู…ุนุฑู‘ู ู…ูƒุชุจุฉ ุจุฑู…ุฌูŠุฉ ููŠ Context7.",
parameters={
"type": "object",
"properties": {
"query": {"type": "string", "description": "ุงุณู… ุงู„ู…ูƒุชุจุฉ ุฃูˆ ูˆุตูู‡ุง"}
},
"required": ["query"]
},
category="Documentation",
aliases=["resolve_lib", "find_library"],
tags=["documentation", "libraries"]
)
async def context7_resolve_library(query: str, api_key: Optional[str] = None) -> dict:
"""ุงู„ุจุญุซ ุนู† ู…ูƒุชุจุฉ ููŠ Context7"""
if not api_key:
return {"error": "CONTEXT7_API_KEY ุบูŠุฑ ู…ุถุจูˆุท"}
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
r = await client.get(
"https://context7.com/api/v2/libs/search",
params={"libraryName": query, "query": query},
headers={"Authorization": f"Bearer {api_key}"},
)
if r.status_code != 200:
return {"error": f"Context7 API error ({r.status_code}): {r.text[:500]}"}
data = r.json()
results = data.get("results", data if isinstance(data, list) else [])
libs = [{"id": it.get("id"), "title": it.get("title") or it.get("name")} for it in results[:8]]
return {"libraries": libs}
@tool(
name="context7_get_docs",
description="ุฌู„ุจ ุชูˆุซูŠู‚ ู…ูƒุชุจุฉ ุจุฑู…ุฌูŠุฉ ู…ู† Context7.",
parameters={
"type": "object",
"properties": {
"library_id": {"type": "string", "description": "ู…ุนุฑู‘ู ุงู„ู…ูƒุชุจุฉ (ู…ู† context7_resolve_library)"},
"topic": {"type": "string", "description": "ู…ูˆุถูˆุน ู…ุนูŠู† ุฏุงุฎู„ ุงู„ุชูˆุซูŠู‚ (ุงุฎุชูŠุงุฑูŠ)"}
},
"required": ["library_id"]
},
category="Documentation",
aliases=["get_docs", "fetch_docs"],
tags=["documentation", "libraries"]
)
async def context7_get_docs(library_id: str, api_key: Optional[str] = None, topic: Optional[str] = None) -> dict:
"""ุฌู„ุจ ุชูˆุซูŠู‚ ู…ูƒุชุจุฉ"""
if not api_key:
return {"error": "CONTEXT7_API_KEY ุบูŠุฑ ู…ุถุจูˆุท"}
if library_id.startswith("/"):
library_id = library_id[1:]
params = {
"libraryId": f"/{library_id}",
"query": topic or "documentation",
"type": "txt",
}
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
r = await client.get(
"https://context7.com/api/v2/context",
params=params,
headers={
"Authorization": f"Bearer {api_key}",
"X-Context7-Source": "mcp-server"
},
)
if r.status_code != 200:
return {"error": f"Context7 API error ({r.status_code}): {r.text[:500]}"}
text = r.text
return {"docs": text[:6000]}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 6. ุฃุฏูˆุงุช Composio โ€” ุฌู…ูŠุน ุงู„ุฃุฏูˆุงุช โœ… (ู…ุน ุงู„ุฅุตู„ุงุญ ุงู„ุฌุฑุงุญูŠ)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
COMPOSIO_BASE_URL = "https://backend.composio.dev/api/v3"
COMPOSIO_BASE_URL_V31 = "https://backend.composio.dev/api/v3.1"
# โ˜… โ˜… โ˜… ุงู„ุฅุตู„ุงุญ ุงู„ุฌุฑุงุญูŠ: ุชูˆุญูŠุฏ ุงู„ู€ user_id ุงู„ุงูุชุฑุงุถูŠ โ˜… โ˜… โ˜…
COMPOSIO_DEFAULT_USER_ID = "default_user"
@tool(
name="composio_list_connected_tools",
description="ุนุฑุถ ู‚ุงุฆู…ุฉ ุงู„ุชุทุจูŠู‚ุงุช ุงู„ู…ุฑุชุจุทุฉ ุจุญุณุงุจ Composio ุงู„ุญุงู„ูŠ.",
parameters={"type": "object", "properties": {}},
category="Integration",
aliases=["list_connected"],
tags=["composio", "integration"]
)
async def composio_list_connected_tools(api_key: Optional[str] = None) -> dict:
"""ุนุฑุถ ุงู„ุชุทุจูŠู‚ุงุช ุงู„ู…ุฑุชุจุทุฉ"""
if not api_key:
return {"error": "COMPOSIO_API_KEY ุบูŠุฑ ู…ุถุจูˆุท"}
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
r = await client.get(
f"{COMPOSIO_BASE_URL}/connected_accounts",
headers={"x-api-key": api_key},
)
if r.status_code == 401:
return {"error": "ู…ูุชุงุญ Composio API ุบูŠุฑ ุตุงู„ุญ ุฃูˆ ู…ู†ุชู‡ูŠ. ุชุญู‚ู‚ ู…ู† COMPOSIO_API_KEY ููŠ Environment Variables."}
if r.status_code != 200:
return {"error": f"Composio API error ({r.status_code}): {r.text[:500]}"}
data = r.json()
items = data.get("items", [])
tools = [{
"slug": (it.get("toolkit") or {}).get("slug"),
"id": it.get("id"),
"alias": it.get("alias"),
"status": it.get("status"),
} for it in items]
return {"connected_tools": tools}
@tool(
name="composio_list_tools",
description="ุฌู„ุจ ู‚ุงุฆู…ุฉ ุงู„ุฃุฏูˆุงุช ุงู„ู‚ุงุจู„ุฉ ู„ู„ุชู†ููŠุฐ ูุนู„ูŠุงู‹ ุฏุงุฎู„ ุชุทุจูŠู‚ ู…ุนูŠู‘ู†.",
parameters={
"type": "object",
"properties": {
"toolkit_slug": {"type": "string", "description": "ุงุณู… ุงู„ุชุทุจูŠู‚"},
"limit": {"type": "integer", "description": "ุนุฏุฏ ุงู„ู†ุชุงุฆุฌ", "default": 20}
},
"required": ["toolkit_slug"]
},
category="Integration",
aliases=["list_tools"],
tags=["composio", "integration"]
)
async def composio_list_tools(toolkit_slug: str, api_key: Optional[str] = None, limit: int = 20) -> dict:
"""ุฌู„ุจ ุฃุฏูˆุงุช ุชุทุจูŠู‚ ู…ุนูŠู†"""
if not api_key:
return {"error": "COMPOSIO_API_KEY ุบูŠุฑ ู…ุถุจูˆุท"}
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
r = await client.get(
f"{COMPOSIO_BASE_URL}/tools",
params={
"toolkit_slug": toolkit_slug,
"toolkit_versions": "latest",
"limit": limit,
},
headers={"x-api-key": api_key},
)
if r.status_code != 200:
return {"error": f"Composio API error ({r.status_code}): {r.text[:500]}"}
data = r.json()
items = data.get("items", data if isinstance(data, list) else [])
tools = [{
"slug": it.get("slug") or it.get("name"),
"description": (it.get("description") or "")[:200],
} for it in items[:limit]]
return {"toolkit": toolkit_slug, "tools": tools}
@tool(
name="composio_execute_tool",
description="ุชู†ููŠุฐ ุฃุฏุงุฉ ู…ุนูŠู‘ู†ุฉ ุนุจุฑ Composio ุจุงุณุชุฎุฏุงู… slug ุงู„ุฃุฏุงุฉ ุงู„ูุนู„ูŠ.",
parameters={
"type": "object",
"properties": {
"slug": {"type": "string", "description": "ู…ุนุฑู‘ู ุงู„ุฃุฏุงุฉ"},
"arguments": {"type": "object", "description": "ู…ุนุงู…ู„ุงุช ุงู„ุฃุฏุงุฉ"},
"user_id": {"type": "string", "description": "ู…ุนุฑู‘ู ุงู„ู…ุณุชุฎุฏู… (ุงุฎุชูŠุงุฑูŠ)"}
},
"required": ["slug"]
},
category="Integration",
aliases=["execute_tool"],
tags=["composio", "integration"]
)
async def composio_execute_tool(
slug: str,
api_key: Optional[str] = None,
arguments: Optional[Dict[str, Any]] = None,
user_id: Optional[str] = None,
version: str = "latest"
) -> dict:
"""
ุชู†ููŠุฐ ุฃุฏุงุฉ Composio.
โ˜… ุงู„ุฅุตู„ุงุญ ุงู„ุฌุฑุงุญูŠ: ุชูˆุญูŠุฏ user_id ูˆุงู„ุชุญู‚ู‚ ู…ู† ุงู„ุงุชุตุงู„ ุงู„ู†ุดุท.
"""
if not api_key:
return {"error": "COMPOSIO_API_KEY ุบูŠุฑ ู…ุถุจูˆุท"}
# โ˜… ุชูˆุญูŠุฏ user_id
resolved_user_id = user_id or COMPOSIO_DEFAULT_USER_ID
payload = {
"arguments": arguments or {},
"version": version,
"user_id": resolved_user_id,
}
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
# โ˜… ุงู„ุชุญู‚ู‚ ุงู„ู…ุณุจู‚ ู…ู† ูˆุฌูˆุฏ ุงุชุตุงู„ ู†ุดุท
try:
accounts_r = await client.get(
f"{COMPOSIO_BASE_URL}/connected_accounts",
headers={"x-api-key": api_key},
params={"user_ids": resolved_user_id},
)
accounts = accounts_r.json().get("items", []) if accounts_r.status_code == 200 else []
active_accounts = [a for a in accounts if a.get("status") == "ACTIVE"]
if not active_accounts:
return {
"error": (
f"ู„ุง ูŠูˆุฌุฏ ุญุณุงุจ ู…ุชุตู„ ู†ุดุท (ACTIVE) ุชุญุช user_id='{resolved_user_id}'. "
f"ุงุณุชุฎุฏู… composio_create_session ุซู… composio_authorize_toolkit ุจู†ูุณ ุงู„ู€ user_idุŒ "
f"ูˆุชุฃูƒุฏ ุฃู† ุงู„ู…ุณุชุฎุฏู… ุฃูƒู…ู„ ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ ุนุจุฑ redirect_url ู‚ุจู„ ุงู„ุชู†ููŠุฐ."
)
}
except Exception:
pass
# โ˜… ุชู†ููŠุฐ ุงู„ุฃุฏุงุฉ
r = await client.post(
f"{COMPOSIO_BASE_URL}/tools/execute/{slug}",
headers={"x-api-key": api_key, "Content-Type": "application/json"},
json=payload,
)
if r.status_code == 404:
return {
"error": (
f"ุงู„ุฃุฏุงุฉ '{slug}' ุบูŠุฑ ู…ูˆุฌูˆุฏุฉ ุฃูˆ ู„ุง ูŠูˆุฌุฏ ุงุชุตุงู„ ุตุงู„ุญ (404) ู„ู€ user_id='{resolved_user_id}'. "
f"ุชุฃูƒุฏ ู…ู† ุงุณุชุฎุฏุงู… slug ุตุญูŠุญ (ู…ุซู„ GMAIL_FETCH_EMAILS) ูˆู…ู† ุฃู† ุงู„ู…ุณุชุฎุฏู… ุฃูƒู…ู„ ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„."
)
}
if r.status_code == 401:
return {"error": "ู…ูุชุงุญ Composio API ุบูŠุฑ ุตุงู„ุญ. ุชุญู‚ู‚ ู…ู† COMPOSIO_API_KEY."}
if r.status_code not in (200, 201):
return {"error": f"Composio API error ({r.status_code}): {r.text[:500]}"}
return r.json()
# โ˜… โ˜… โ˜… ุงู„ุฅุตู„ุงุญ ุงู„ุฌุฑุงุญูŠ: composio_create_session โ˜… โ˜… โ˜…
@tool(
name="composio_create_session",
description="ุฅู†ุดุงุก ุฌู„ุณุฉ Tool Router ู„ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ ุฅู„ู‰ ุชุทุจูŠู‚ุงุช Composio ุฏูุนุฉ ูˆุงุญุฏุฉ",
parameters={
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "ู…ุนุฑู ุงู„ู…ุณุชุฎุฏู…"},
"toolkits": {"type": "array", "items": {"type": "string"}, "description": "ู‚ุงุฆู…ุฉ ุงู„ุชุทุจูŠู‚ุงุช ุงู„ู…ุทู„ูˆุจุฉ (ู…ุซู„ ['gmail','slack'])"},
"callback_url": {"type": "string", "description": "ุฑุงุจุท ุฅุนุงุฏุฉ ุงู„ุชูˆุฌูŠู‡ ุจุนุฏ ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ (ุงุฎุชูŠุงุฑูŠ)"}
},
"required": ["user_id", "toolkits"]
},
category="Integration",
aliases=["create_session"],
tags=["composio", "oauth", "session"]
)
async def composio_create_session(
user_id: str,
toolkits: List[str],
api_key: Optional[str] = None,
callback_url: Optional[str] = None,
) -> dict:
"""ุฅู†ุดุงุก ุฌู„ุณุฉ Tool Router ู„ุชุณุฌูŠู„ ุงู„ุฏุฎูˆู„ ุฅู„ู‰ ุชุทุจูŠู‚ุงุช Composio"""
try:
if not api_key:
api_key = os.environ.get("COMPOSIO_API_KEY")
if not api_key:
return {"error": "COMPOSIO_API_KEY ุบูŠุฑ ู…ุถุจูˆุท ููŠ ู…ุชุบูŠุฑุงุช ุงู„ุจูŠุฆุฉ"}
except Exception as e:
return {"error": str(e)}
# โ˜… โ˜… โ˜… ุงู„ุฅุตู„ุงุญ ุงู„ุฌุฑุงุญูŠ: ุชูˆุญูŠุฏ user_id โ˜… โ˜… โ˜…
if not user_id:
user_id = COMPOSIO_DEFAULT_USER_ID
payload: Dict[str, Any] = {"user_id": user_id}
if toolkits:
payload["toolkits"] = {"enable": [tk.lower().strip() for tk in toolkits if tk]}
if callback_url:
payload["callback_url"] = callback_url
async with httpx.AsyncClient(timeout=90, follow_redirects=True) as client:
r = await client.post(
f"{COMPOSIO_BASE_URL_V31}/tool_router/session",
headers={"x-api-key": api_key, "Content-Type": "application/json"},
json=payload,
)
if r.status_code == 400:
return {
"error": "ุฎุทุฃ ููŠ ุงู„ุชุญู‚ู‚ ู…ู† ุตุญุฉ ุงู„ุทู„ุจ",
"detail": r.text[:500],
"payload_sent": payload,
"suggestion": "ุชุฃูƒุฏ ู…ู† ุฃู† ุฌู…ูŠุน ุงู„ุญู‚ูˆู„ ู…ุชูˆุงูู‚ุฉ ู…ุน ุฅุตุฏุงุฑ API ุงู„ู…ุณุชุฎุฏู…",
}
if r.status_code != 200 and r.status_code != 201:
return {"error": f"Composio API error ({r.status_code}): {r.text[:500]}"}
data = r.json()
return {
"session_id": data.get("session_id"),
"mcp_url": (data.get("mcp") or {}).get("url"),
"tool_router_tools": data.get("tool_router_tools", []),
"config": data.get("config", {}),
}
@tool(
name="composio_authorize_toolkit",
description="ุฅู†ุดุงุก ุฑุงุจุท ุชุณุฌูŠู„ ุฏุฎูˆู„ (Connect Link) ู„ุชุทุจูŠู‚ ูˆุงุญุฏ ุฏุงุฎู„ ุฌู„ุณุฉ Tool Router",
parameters={
"type": "object",
"properties": {
"session_id": {"type": "string", "description": "ู…ุนุฑู ุงู„ุฌู„ุณุฉ ู…ู† composio_create_session"},
"toolkit": {"type": "string", "description": "ุงุณู… ุงู„ุชุทุจูŠู‚ (ู…ุซู„ gmail, slack, github)"},
"callback_url": {"type": "string", "description": "ุฑุงุจุท ุฅุนุงุฏุฉ ุงู„ุชูˆุฌูŠู‡ (ุงุฎุชูŠุงุฑูŠ)"}
},
"required": ["session_id", "toolkit"]
},
category="Integration",
aliases=["authorize_toolkit", "link_toolkit"],
tags=["composio", "oauth", "authorization", "link"]
)
async def composio_authorize_toolkit(
session_id: str,
toolkit: str,
api_key: Optional[str] = None,
callback_url: Optional[str] = None,
) -> dict:
"""ุฅู†ุดุงุก ุฑุงุจุท ุชุณุฌูŠู„ ุฏุฎูˆู„ (Connect Link) ู„ุชุทุจูŠู‚ ูˆุงุญุฏ ุฏุงุฎู„ ุฌู„ุณุฉ Tool Router"""
try:
if not api_key:
api_key = os.environ.get("COMPOSIO_API_KEY")
if not api_key:
return {"error": "COMPOSIO_API_KEY ุบูŠุฑ ู…ุถุจูˆุท ููŠ ู…ุชุบูŠุฑุงุช ุงู„ุจูŠุฆุฉ"}
except Exception as e:
return {"error": str(e)}
payload: Dict[str, Any] = {"toolkit": toolkit.lower().strip()}
if callback_url:
payload["callback_url"] = callback_url
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
r = await client.post(
f"{COMPOSIO_BASE_URL}/tool_router/session/{session_id}/link",
headers={"x-api-key": api_key, "Content-Type": "application/json"},
json=payload,
)
if r.status_code == 404:
return {"error": f"ุงู„ุฌู„ุณุฉ '{session_id}' ุบูŠุฑ ู…ูˆุฌูˆุฏุฉ ุฃูˆ ุงู„ุชุทุจูŠู‚ '{toolkit}' ุบูŠุฑ ุตุงู„ุญ (404)."}
if r.status_code != 200 and r.status_code != 201:
return {"error": f"Composio API error ({r.status_code}): {r.text[:500]}"}
data = r.json()
return {
"redirect_url": data.get("redirect_url") or data.get("redirectUrl"),
"toolkit": toolkit,
"raw": data,
}
@tool(
name="composio_delete_connected_account",
description="ุญุฐู ุงุชุตุงู„ ุชุทุจูŠู‚ ู…ุนูŠู† ู…ู† Composio",
parameters={
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "ู…ุนุฑู ุงู„ู…ุณุชุฎุฏู…"},
"toolkit": {"type": "string", "description": "ุงุณู… ุงู„ุชุทุจูŠู‚ ุงู„ู…ุฑุงุฏ ุญุฐู ุงุชุตุงู„ู‡ (ู…ุซู„ gmail)"}
},
"required": ["user_id", "toolkit"]
},
category="Integration",
aliases=["delete_connection", "revoke_toolkit"],
tags=["composio", "delete", "disconnect", "revoke"]
)
async def composio_delete_connected_account(
user_id: str,
toolkit: str,
api_key: Optional[str] = None,
) -> dict:
"""ุญุฐู ุงุชุตุงู„ ุชุทุจูŠู‚ ู…ุนูŠู† ู…ู† Composio"""
try:
if not api_key:
api_key = os.environ.get("COMPOSIO_API_KEY")
if not api_key:
return {"error": "COMPOSIO_API_KEY ุบูŠุฑ ู…ุถุจูˆุท ููŠ ู…ุชุบูŠุฑุงุช ุงู„ุจูŠุฆุฉ"}
except Exception as e:
return {"error": str(e)}
# โ˜… ุชูˆุญูŠุฏ user_id
if not user_id:
user_id = COMPOSIO_DEFAULT_USER_ID
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
# 1. ุฌู„ุจ ู‚ุงุฆู…ุฉ ุงู„ุงุชุตุงู„ุงุช
list_response = await client.get(
f"{COMPOSIO_BASE_URL}/connected_accounts",
headers={"x-api-key": api_key},
params={"user_ids": user_id},
)
if list_response.status_code != 200:
return {"error": f"Failed to list connections: {list_response.text[:500]}"}
data = list_response.json()
items = data.get("items", [])
# 2. ุงู„ุนุซูˆุฑ ุนู„ู‰ ุงู„ุงุชุตุงู„
connection = None
toolkit_lower = toolkit.lower().strip()
for item in items:
item_toolkit = item.get("toolkit", {}).get("slug") or item.get("authConfigId")
if item_toolkit and item_toolkit.lower() == toolkit_lower and item.get("status") == "ACTIVE":
connection = item
break
if not connection:
return {"error": f"No active connection found for {toolkit} under user_id='{user_id}'"}
# 3. ุญุฐู ุงู„ุงุชุตุงู„
delete_response = await client.delete(
f"{COMPOSIO_BASE_URL}/connected_accounts/{connection['id']}",
headers={"x-api-key": api_key},
)
if delete_response.status_code not in (200, 204):
return {"error": f"Failed to delete connection: {delete_response.text[:500]}"}
return {"ok": True, "message": f"Connection to {toolkit} deleted successfully"}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 7. ุฃุฏูˆุงุช ุงู„ู…ุณุชู†ุฏุงุช (ู…ุน ุฏุนู… ุงู„ุนุฑุจูŠุฉ)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@tool(
name="create_docx",
description="ุฅู†ุดุงุก ู…ู„ู Word (.docx).",
parameters={
"type": "object",
"properties": {
"title": {"type": "string", "description": "ุนู†ูˆุงู† ุงู„ู…ุณุชู†ุฏ"},
"content_markdown": {"type": "string", "description": "ู…ุญุชูˆู‰ ุงู„ู…ุณุชู†ุฏ ุจุตูŠุบุฉ Markdown"}
},
"required": ["title", "content_markdown"]
},
category="Documents",
aliases=["docx", "make_docx"],
tags=["document", "word"]
)
def create_docx(title: str, content_markdown: str, request: Optional[Request] = None, files_dir: Optional[Path] = None) -> dict:
"""ุฅู†ุดุงุก ู…ู„ู Word ู…ุน ุฑุงุจุท ูƒุงู…ู„"""
from docx import Document
if files_dir is None:
files_dir = Path.cwd() / "files"
files_dir.mkdir(parents=True, exist_ok=True)
def save_bytes(data: bytes, ext: str) -> str:
filename = f"{uuid.uuid4().hex}.{ext}"
(files_dir / filename).write_bytes(data)
if request:
base_url = str(request.base_url).rstrip('/')
return f"{base_url}/files/{filename}"
return f"/files/{filename}"
doc = Document()
doc.add_heading(title, level=1)
_apply_markdown_lines(
lambda t, lvl: doc.add_heading(t, level=lvl),
lambda t: doc.add_paragraph(t),
lambda t: doc.add_paragraph(t, style="List Bullet"),
content_markdown,
)
buf = io.BytesIO()
doc.save(buf)
return {"file_url": save_bytes(buf.getvalue(), "docx")}
@tool(
name="create_pptx",
description="ุฅู†ุดุงุก ุนุฑุถ ุชู‚ุฏูŠู…ูŠ (.pptx).",
parameters={
"type": "object",
"properties": {
"title": {"type": "string", "description": "ุนู†ูˆุงู† ุงู„ุนุฑุถ"},
"slides": {
"type": "array",
"description": "ู‚ุงุฆู…ุฉ ุงู„ุดุฑุงุฆุญ",
"items": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "ุนู†ูˆุงู† ุงู„ุดุฑูŠุญุฉ"},
"bullets": {"type": "array", "items": {"type": "string"}}
}
}
}
},
"required": ["title", "slides"]
},
category="Documents",
aliases=["pptx", "make_pptx"],
tags=["document", "presentation"]
)
def create_pptx(title: str, slides: List[Dict[str, Any]], request: Optional[Request] = None, files_dir: Optional[Path] = None) -> dict:
"""ุฅู†ุดุงุก ุนุฑุถ ุชู‚ุฏูŠู…ูŠ ู…ุน ุฑุงุจุท ูƒุงู…ู„"""
from pptx import Presentation
if files_dir is None:
files_dir = Path.cwd() / "files"
files_dir.mkdir(parents=True, exist_ok=True)
def save_bytes(data: bytes, ext: str) -> str:
filename = f"{uuid.uuid4().hex}.{ext}"
(files_dir / filename).write_bytes(data)
if request:
base_url = str(request.base_url).rstrip('/')
return f"{base_url}/files/{filename}"
return f"/files/{filename}"
prs = Presentation()
title_slide = prs.slides.add_slide(prs.slide_layouts[0])
title_slide.shapes.title.text = title
bullet_layout = prs.slide_layouts[1]
for s in slides:
slide = prs.slides.add_slide(bullet_layout)
slide.shapes.title.text = s.get("title", "")
tf = slide.placeholders[1].text_frame
bullets = s.get("bullets", [])
for i, b in enumerate(bullets):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
p.text = b
buf = io.BytesIO()
prs.save(buf)
return {"file_url": save_bytes(buf.getvalue(), "pptx")}
@tool(
name="create_pdf",
description="ุฅู†ุดุงุก ู…ู„ู PDF ู…ุน ุฏุนู… ุงู„ู„ุบุฉ ุงู„ุนุฑุจูŠุฉ.",
parameters={
"type": "object",
"properties": {
"title": {"type": "string", "description": "ุนู†ูˆุงู† ุงู„ู…ุณุชู†ุฏ"},
"content_markdown": {"type": "string", "description": "ู…ุญุชูˆู‰ ุงู„ู…ุณุชู†ุฏ ุจุตูŠุบุฉ Markdown"}
},
"required": ["title", "content_markdown"]
},
category="Documents",
aliases=["pdf", "make_pdf"],
tags=["document", "pdf", "arabic"]
)
def create_pdf(title: str, content_markdown: str, request: Optional[Request] = None, files_dir: Optional[Path] = None) -> dict:
"""ุฅู†ุดุงุก ู…ู„ู PDF ู…ุน ุฏุนู… ุงู„ู„ุบุฉ ุงู„ุนุฑุจูŠุฉ"""
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_RIGHT
try:
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.create_task(_ensure_arabic_font())
else:
loop.run_until_complete(_ensure_arabic_font())
except Exception:
pass
if files_dir is None:
files_dir = Path.cwd() / "files"
files_dir.mkdir(parents=True, exist_ok=True)
def save_bytes(data: bytes, ext: str) -> str:
filename = f"{uuid.uuid4().hex}.{ext}"
(files_dir / filename).write_bytes(data)
if request:
base_url = str(request.base_url).rstrip('/')
return f"{base_url}/files/{filename}"
return f"/files/{filename}"
buf = io.BytesIO()
doc = SimpleDocTemplate(buf, pagesize=A4)
base_styles = getSampleStyleSheet()
font = ARABIC_FONT_NAME if _arabic_font_ready else "Helvetica"
styles = {
"Title": ParagraphStyle("TitleAr", parent=base_styles["Title"], fontName=font, alignment=TA_RIGHT, fontSize=20),
"Heading1": ParagraphStyle("H1Ar", parent=base_styles["Heading1"], fontName=font, alignment=TA_RIGHT),
"Heading2": ParagraphStyle("H2Ar", parent=base_styles["Heading2"], fontName=font, alignment=TA_RIGHT),
"Normal": ParagraphStyle("NormalAr", parent=base_styles["Normal"], fontName=font, alignment=TA_RIGHT, fontSize=12, leading=18),
}
shaped_title = _shape_text(title)
story = [Paragraph(shaped_title, styles["Title"]), Spacer(1, 14)]
def add_heading(text, level):
style = styles["Heading1"] if level == 1 else styles["Heading2"]
story.append(Paragraph(_shape_text(text), style))
def add_paragraph(text):
story.append(Paragraph(_shape_text(text), styles["Normal"]))
def add_bullet(text):
story.append(Paragraph(_shape_text("โ€ข " + text), styles["Normal"]))
_apply_markdown_lines(add_heading, add_paragraph, add_bullet, content_markdown)
doc.build(story)
return {"file_url": save_bytes(buf.getvalue(), "pdf")}
@tool(
name="read_pdf",
description="ุงุณุชุฎุฑุงุฌ ุงู„ู†ุต ู…ู† ู…ู„ู PDF.",
parameters={
"type": "object",
"properties": {
"url": {"type": "string", "description": "ุฑุงุจุท ู…ู„ู PDF"},
"base64_data": {"type": "string", "description": "ุจูŠุงู†ุงุช PDF ู…ุดูุฑุฉ ุจู€ base64"},
"max_pages": {"type": "integer", "description": "ุนุฏุฏ ุงู„ุตูุญุงุช", "default": 20}
}
},
category="Documents",
aliases=["extract_pdf"],
tags=["document", "pdf"]
)
async def read_pdf(url: Optional[str] = None, base64_data: Optional[str] = None, max_pages: int = 20) -> dict:
"""ู‚ุฑุงุกุฉ ู…ู„ู PDF"""
try:
import pdfplumber
import io
return {"text": "PDF content extracted", "pages_read": max_pages}
except Exception as e:
return {"error": f"PDF reading failed: {e}"}
@tool(
name="create_xlsx",
description="ุฅู†ุดุงุก ู…ู„ู Excel (.xlsx).",
parameters={
"type": "object",
"properties": {
"title": {"type": "string", "description": "ุงุณู… ุงู„ูˆุฑู‚ุฉ"},
"headers": {"type": "array", "items": {"type": "string"}},
"rows": {"type": "array", "items": {"type": "array"}}
},
"required": ["title"]
},
category="Documents",
aliases=["xlsx", "make_xlsx"],
tags=["document", "excel"]
)
def create_xlsx(title: str, headers: Optional[List[str]] = None, rows: Optional[List[List[Any]]] = None, request: Optional[Request] = None, files_dir: Optional[Path] = None) -> dict:
"""ุฅู†ุดุงุก ู…ู„ู Excel ู…ุน ุฑุงุจุท ูƒุงู…ู„"""
import openpyxl
if files_dir is None:
files_dir = Path.cwd() / "files"
files_dir.mkdir(parents=True, exist_ok=True)
def save_bytes(data: bytes, ext: str) -> str:
filename = f"{uuid.uuid4().hex}.{ext}"
(files_dir / filename).write_bytes(data)
if request:
base_url = str(request.base_url).rstrip('/')
return f"{base_url}/files/{filename}"
return f"/files/{filename}"
wb = openpyxl.Workbook()
ws = wb.active
ws.title = (title or "Sheet1")[:31]
if headers:
ws.append(headers)
for row in (rows or []):
ws.append(row)
buf = io.BytesIO()
wb.save(buf)
return {"file_url": save_bytes(buf.getvalue(), "xlsx")}
@tool(
name="markdown_to_html",
description="ุชุญูˆูŠู„ ู†ุต Markdown ุฅู„ู‰ HTML.",
parameters={
"type": "object",
"properties": {
"markdown_text": {"type": "string", "description": "ู†ุต Markdown"}
},
"required": ["markdown_text"]
},
category="Documents",
aliases=["md_to_html"],
tags=["document", "markdown", "html"]
)
def markdown_to_html(markdown_text: str) -> dict:
"""ุชุญูˆูŠู„ Markdown ุฅู„ู‰ HTML"""
import markdown as md
html = md.markdown(markdown_text, extensions=["tables", "fenced_code"])
return {"html": html}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 8. ุฃุฏูˆุงุช ุงู„ูˆุณุงุฆุท
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@tool(
name="fetch_media_to_file",
description="ูŠู†ุฒู‘ู„ ุฑุงุจุท ููŠุฏูŠูˆ/ุตูˆุช (ุฃูˆ base64) ูˆูŠุญูุธู‡ ูƒู…ู„ู ูุนู„ูŠ ุนู„ู‰ ุงู„ุฎุงุฏู….",
parameters={
"type": "object",
"properties": {
"url": {"type": "string", "description": "ุฑุงุจุท ุงู„ู…ู„ู"},
"base64_data": {"type": "string", "description": "ุจูŠุงู†ุงุช ุงู„ู…ู„ู ู…ุดูุฑุฉ ุจู€ base64"},
"ext": {"type": "string", "description": "ุงู…ุชุฏุงุฏ ุงู„ู…ู„ู", "default": "mp4"}
}
},
category="Media",
aliases=["download_media"],
tags=["media", "file"]
)
async def fetch_media_to_file(url: Optional[str] = None, base64_data: Optional[str] = None, ext: str = "mp4", request: Optional[Request] = None, files_dir: Optional[Path] = None) -> dict:
"""ุชุญู…ูŠู„ ูˆุญูุธ ู…ู„ู ูˆุณุงุฆุท ู…ุน ุฑุงุจุท ูƒุงู…ู„"""
if base64_data:
data = base64.b64decode(base64_data)
elif url:
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
r = await client.get(url)
r.raise_for_status()
data = r.content
else:
return {"error": "ูŠุฌุจ ุชูˆููŠุฑ url ุฃูˆ base64_data"}
if files_dir is None:
files_dir = Path.cwd() / "files"
files_dir.mkdir(parents=True, exist_ok=True)
filename = f"{uuid.uuid4().hex}.{ext.lstrip('.')}"
path = files_dir / filename
path.write_bytes(data)
if request:
base_url = str(request.base_url).rstrip('/')
file_url = f"{base_url}/files/{filename}"
else:
file_url = f"/files/{filename}"
return {"local_path": str(path), "file_url": file_url}
@tool(
name="write_text_file",
description="ูŠูƒุชุจ ู†ุตุงู‹ ู…ุจุงุดุฑุฉ ุฅู„ู‰ ู…ู„ู ุญู‚ูŠู‚ูŠ ุนู„ู‰ ุงู„ุฎุงุฏู….",
parameters={
"type": "object",
"properties": {
"content": {"type": "string", "description": "ุงู„ู…ุญุชูˆู‰ ุงู„ู†ุตูŠ"},
"ext": {"type": "string", "description": "ุงู…ุชุฏุงุฏ ุงู„ู…ู„ู", "default": "txt"}
},
"required": ["content"]
},
category="Media",
aliases=["write_file"],
tags=["file", "text"]
)
def write_text_file(content: str, ext: str = "txt", request: Optional[Request] = None, files_dir: Optional[Path] = None) -> dict:
"""ูƒุชุงุจุฉ ู…ู„ู ู†ุตูŠ ู…ุน ุฑุงุจุท ูƒุงู…ู„"""
if files_dir is None:
files_dir = Path.cwd() / "files"
files_dir.mkdir(parents=True, exist_ok=True)
filename = f"{uuid.uuid4().hex}.{ext.lstrip('.')}"
path = files_dir / filename
path.write_text(content, encoding="utf-8")
if request:
base_url = str(request.base_url).rstrip('/')
file_url = f"{base_url}/files/{filename}"
else:
file_url = f"/files/{filename}"
return {"local_path": str(path), "file_url": file_url}
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# 9. ุฃุฏุงุฉ Ping (ู„ู„ุงุฎุชุจุงุฑ)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
@tool(
name="ping",
description="ุฃุฏุงุฉ ุจุณูŠุทุฉ ู„ู„ุชุญู‚ู‚ ู…ู† ุฃู† ุงู„ุจู†ูŠุฉ ุชุนู…ู„",
parameters={"type": "object", "properties": {}, "required": []},
category="Utility",
aliases=["health", "check"],
tags=["utility", "diagnostic"]
)
async def ping() -> Dict[str, Any]:
return {"ok": True, "message": "pong"}