File size: 5,729 Bytes
293ab16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import datetime
import random
import logging
import contextlib
import io
from typing import Any, Callable, Dict, List, Tuple, Union, Type

from pydantic import BaseModel, Field
from playwright.sync_api import sync_playwright
import duckduckgo_search

# External modules (you must have these implemented)
from app.email_tool import send_email, generate_email
from app.repl_tool import run_python_code
from app.translation_tool import translate
from app.vision import describe_image

from langchain.agents import Tool

# === Logging Setup ===
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

# === Structured Input Schemas ===
class ExecInput(BaseModel):
    code: str = Field(..., description="Python code to execute")

class WebSearchInput(BaseModel):
    url: str = Field(..., description="URL to browse for content")

# === Python Code Execution ===
def exec_py(inputs: ExecInput) -> str:
    """
    Securely execute Python code and return output or error.
    """
    output = io.StringIO()
    local_vars = {}

    try:
        with contextlib.redirect_stdout(output):
            exec(inputs.code, {}, local_vars)
        return output.getvalue().strip() or "✅ Code executed successfully."
    except Exception as e:
        logger.error(f"exec_py error: {e}")
        return f"❌ Error: {e}"

# === Simple Tools ===
def calc(expression: str) -> str:
    """
    Evaluate a simple math expression.
    """
    try:
        result = eval(expression, {"__builtins__": None}, {})
        return str(result)
    except Exception as e:
        logger.error(f"Calc error for '{expression}': {e}")
        return "❌ Invalid expression"

def time_now(_: str = "") -> str:
    return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def joke(_: str = "") -> str:
    return random.choice([
        "Parallel lines have so much in common—they’ll never meet.",
        "Why don’t scientists trust atoms? Because they make up everything!",
        "I told my computer I needed a break, and it said no problem — it needed one too."
    ])

def browse(inputs: WebSearchInput) -> str:
    """
    Fetch and return the text content of a web page.
    """
    try:
        with sync_playwright() as pw:
            browser = pw.chromium.launch(headless=True)
            page = browser.new_page()
            page.goto(inputs.url, timeout=10000)
            text = page.text_content("body") or ""
            browser.close()
            return text[:500] if text else "❌ No content found."
    except Exception as e:
        logger.error(f"Browse error: {e}")
        return f"❌ Error browsing: {e}"

def ddg_search(query: str) -> List[str]:
    try:
        results = duckduckgo_search.DuckDuckGoSearch().text(query)
        return results[:3] if results else ["No results"]
    except Exception as e:
        logger.error(f"DuckDuckGo search error: {e}")
        return ["❌ Search failed"]

# === LangChain-Formatted Tool List ===
def get_tools() -> List[Tool]:
    return [
        Tool(
            name="Code Interpreter",
            func=run_python_code,
            description="🧠 Executes Python code and returns results.",
        ),
        Tool(
            name="Email Generator",
            func=lambda prompt: generate_email("Customer", prompt, 15),
            description="📧 Generates a promotional or customer email.",
        ),
        Tool(
            name="Translate Text",
            func=translate,
            description="🌐 Translates input text to a selected language.",
        ),
        Tool(
            name="Describe Image",
            func=describe_image,
            description="🖼️ Describes the contents of an image.",
        ),
        Tool(
            name="Browse Website",
            func=browse,
            description="🔎 Browse and fetch page content.",
            args_schema=WebSearchInput,
        ),
        Tool(
            name="Python Executor",
            func=exec_py,
            description="🐍 Execute Python code and return output.",
            args_schema=ExecInput,
        ),
    ]

# === TOOL MAPPING ===
TOOLS: Dict[str, Union[Callable[..., Any], Tuple[Type[BaseModel], Callable[..., Any]]]] = {
    "calc": calc,
    "time": time_now,
    "joke": joke,
    "search": ddg_search,
    "browse": (WebSearchInput, browse),
    "exec_python": (ExecInput, exec_py),
    "send_email": send_email,
    "run_code": run_python_code,
    "translate": translate,
    "describe_image": describe_image,
}

# === Dynamic Tool Execution ===
def use_tool(tool_name: str, *args, **kwargs) -> Any:
    tool = TOOLS.get(tool_name)
    if not tool:
        return {"error": f"Tool '{tool_name}' not found"}

    if isinstance(tool, tuple):
        schema_cls, func = tool
        try:
            input_data = args[0] if args else kwargs
            validated = schema_cls.parse_obj(input_data)
            return func(validated)
        except Exception as e:
            logger.error(f"Validation failed for '{tool_name}': {e}")
            return {"error": f"Invalid input for tool '{tool_name}': {e}"}
    else:
        try:
            return tool(*args, **kwargs)
        except Exception as e:
            logger.error(f"Execution failed for '{tool_name}': {e}")
            return {"error": f"Tool execution failed: {e}"}

# === Dispatcher ===
def tool_dispatch(command: str, args: Any) -> Any:
    if command not in TOOLS:
        return {"error": f"Unknown tool '{command}'"}

    tool = TOOLS[command]
    if isinstance(args, (list, tuple)):
        return use_tool(command, *args)
    elif isinstance(args, dict):
        return use_tool(command, args)
    else:
        return use_tool(command, args)