feat: implement tools and agentic behaviour
Browse files- __init__.py +0 -0
- agent.py +130 -0
- app.py +97 -20
- requirements.txt +145 -2
- tools.py +274 -0
__init__.py
ADDED
|
File without changes
|
agent.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
import pprint
|
| 4 |
+
from llama_index.core.agent.workflow.workflow_events import AgentInput, AgentOutput, AgentStream, ToolCall, ToolCallResult
|
| 5 |
+
from llama_index.core.llms import ChatMessage
|
| 6 |
+
# from llama_index.llms.ollama import Ollama
|
| 7 |
+
from llama_index.core.agent.workflow import AgentWorkflow
|
| 8 |
+
from llama_index.core.tools import FunctionTool
|
| 9 |
+
from workflows.events import StopEvent, StartEvent, StepState, StepStateChanged
|
| 10 |
+
from llama_index.tools.arxiv import ArxivToolSpec
|
| 11 |
+
from llama_index.tools.wikipedia import WikipediaToolSpec
|
| 12 |
+
from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
|
| 13 |
+
from llama_index.llms.nebius import NebiusLLM
|
| 14 |
+
|
| 15 |
+
import tools
|
| 16 |
+
|
| 17 |
+
"""
|
| 18 |
+
TODO: 2. Update gradio app to include HF inference model instead of Ollama.
|
| 19 |
+
TODO: 4. Run HF Space and submit answers to the scoring server.
|
| 20 |
+
TODO: 5. Get a certificate for the course.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
class BasicAgent:
|
| 24 |
+
def __init__(self, verbose: bool = False):
|
| 25 |
+
self.verbose = verbose
|
| 26 |
+
self.system_prompt = """
|
| 27 |
+
You are a general AI assistant. I will ask you a question.
|
| 28 |
+
Report your thoughts, and finish your answer with the following template:
|
| 29 |
+
FINAL ANSWER: [YOUR FINAL ANSWER].
|
| 30 |
+
YOUR FINAL ANSWER should be a number OR as few words as possible
|
| 31 |
+
OR a comma separated list of numbers and/or strings.
|
| 32 |
+
If you are asked for a number, don't use comma to write your number
|
| 33 |
+
neither use units such as $ or percent sign unless specified otherwise.
|
| 34 |
+
If you are asked for a string, don't use articles, neither abbreviations
|
| 35 |
+
(e.g. for cities), and write the digits in plain text unless specified
|
| 36 |
+
otherwise. If you are asked for a comma separated list, apply the above
|
| 37 |
+
rules depending of whether the element to be put in the list is a number
|
| 38 |
+
or a string.
|
| 39 |
+
"""
|
| 40 |
+
# llm = Ollama(
|
| 41 |
+
# model="gpt-oss:20b",
|
| 42 |
+
# request_timeout=120.0
|
| 43 |
+
# )
|
| 44 |
+
# llm = HuggingFaceInferenceAPI(
|
| 45 |
+
# model_name="Qwen/Qwen2.5-Coder-32B-Instruct",
|
| 46 |
+
# )
|
| 47 |
+
llm = NebiusLLM(
|
| 48 |
+
api_key=os.getenv("NEBIUS_API_KEY"),
|
| 49 |
+
model="Qwen/Qwen3-235B-A22B-Instruct-2507",
|
| 50 |
+
api_base="https://api.tokenfactory.nebius.com/v1"
|
| 51 |
+
)
|
| 52 |
+
self.workflow = AgentWorkflow.from_tools_or_functions(
|
| 53 |
+
[
|
| 54 |
+
FunctionTool.from_defaults(tools.multiply),
|
| 55 |
+
FunctionTool.from_defaults(tools.add),
|
| 56 |
+
# FunctionTool.from_defaults(tools.transcribeAudio),
|
| 57 |
+
# FunctionTool.from_defaults(tools.describeImage),
|
| 58 |
+
FunctionTool.from_defaults(tools.directFetchTool),
|
| 59 |
+
FunctionTool.from_defaults(tools.webSearchTool),
|
| 60 |
+
*ArxivToolSpec().to_tool_list(),
|
| 61 |
+
*WikipediaToolSpec().to_tool_list(),
|
| 62 |
+
],
|
| 63 |
+
llm=llm,
|
| 64 |
+
system_prompt=self.system_prompt
|
| 65 |
+
)
|
| 66 |
+
print("BasicAgent initialized.")
|
| 67 |
+
|
| 68 |
+
async def __call__(self, question: str) -> str:
|
| 69 |
+
"""Async call method that returns the final result without streaming"""
|
| 70 |
+
print(f"Agent received question (first 50 chars): {question[:50]}...")
|
| 71 |
+
|
| 72 |
+
if self.verbose:
|
| 73 |
+
return await self.stream_answers(question)
|
| 74 |
+
|
| 75 |
+
answer = await self.workflow.run(user_msg=question)
|
| 76 |
+
print(f"Agent returning answer: {answer}")
|
| 77 |
+
return answer
|
| 78 |
+
|
| 79 |
+
def call_sync(self, question: str) -> str:
|
| 80 |
+
"""Synchronous wrapper for __call__ method (for compatibility with sync code)"""
|
| 81 |
+
return asyncio.run(self.__call__(question))
|
| 82 |
+
|
| 83 |
+
async def stream_answers(self, question: str) -> None:
|
| 84 |
+
|
| 85 |
+
handler = self.workflow.run(user_msg=question, max_iterations=10)
|
| 86 |
+
async for event in handler.stream_events():
|
| 87 |
+
if isinstance(event, AgentInput):
|
| 88 |
+
# print(f"\nAgent input: {event.input}")
|
| 89 |
+
pass
|
| 90 |
+
elif isinstance(event, AgentOutput):
|
| 91 |
+
# print(f"\nAgent output: {event}")
|
| 92 |
+
pass
|
| 93 |
+
elif isinstance(event, ToolCall):
|
| 94 |
+
print(f"\n\tCalled tool: {event.tool_name} {event.tool_kwargs}")
|
| 95 |
+
elif isinstance(event, ToolCallResult):
|
| 96 |
+
print(f"\n\t{event.tool_name} {event.tool_kwargs} -> {event.tool_output}")
|
| 97 |
+
elif isinstance(event, StopEvent):
|
| 98 |
+
return event.result
|
| 99 |
+
elif isinstance(event, AgentStream):
|
| 100 |
+
if event.delta:
|
| 101 |
+
print(event.delta, end="", flush=True)
|
| 102 |
+
elif event.thinking_delta:
|
| 103 |
+
print(event.thinking_delta, end="", flush=True)
|
| 104 |
+
else:
|
| 105 |
+
pprint.pprint(f"Received event: {type(event)} {event}")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
if __name__ == "__main__":
|
| 109 |
+
async def main():
|
| 110 |
+
from dotenv import load_dotenv
|
| 111 |
+
load_dotenv()
|
| 112 |
+
# agent = BasicAgent()
|
| 113 |
+
|
| 114 |
+
llm = NebiusLLM(
|
| 115 |
+
api_key=os.getenv("NEBIUS_API_KEY"),
|
| 116 |
+
model="Qwen/Qwen3-235B-A22B-Instruct-2507",
|
| 117 |
+
api_base="https://api.tokenfactory.nebius.com/v1"
|
| 118 |
+
)
|
| 119 |
+
# result = llm.complete("Hello!")
|
| 120 |
+
result = llm.chat([ChatMessage("Who are you?")])
|
| 121 |
+
print(result)
|
| 122 |
+
|
| 123 |
+
# Example without streaming (using __call__)
|
| 124 |
+
# print("\n=== Non-Streaming Example ===")
|
| 125 |
+
# result = await agent("What is 5 + 7?")
|
| 126 |
+
# print(f"Final result: {result}")
|
| 127 |
+
# result = await agent.stream_answers("Who did the actor who played Ray in the Polish-language version of Everybody Loves Raymond play in Magda M.? Give only the first name.")
|
| 128 |
+
# print(f"\nFinal result: {result}")
|
| 129 |
+
|
| 130 |
+
asyncio.run(main())
|
app.py
CHANGED
|
@@ -3,22 +3,79 @@ import gradio as gr
|
|
| 3 |
import requests
|
| 4 |
import inspect
|
| 5 |
import pandas as pd
|
|
|
|
| 6 |
|
| 7 |
# (Keep Constants as is)
|
| 8 |
# --- Constants ---
|
| 9 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
print(f"
|
| 20 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
| 23 |
"""
|
| 24 |
Fetches all questions, runs the BasicAgent on them, submits all answers,
|
|
@@ -40,7 +97,7 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
|
|
| 40 |
|
| 41 |
# 1. Instantiate Agent ( modify this part to create your agent)
|
| 42 |
try:
|
| 43 |
-
agent = BasicAgent()
|
| 44 |
except Exception as e:
|
| 45 |
print(f"Error instantiating agent: {e}")
|
| 46 |
return f"Error initializing agent: {e}", None
|
|
@@ -55,16 +112,16 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
|
|
| 55 |
response.raise_for_status()
|
| 56 |
questions_data = response.json()
|
| 57 |
if not questions_data:
|
| 58 |
-
|
| 59 |
-
|
| 60 |
print(f"Fetched {len(questions_data)} questions.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
except requests.exceptions.RequestException as e:
|
| 62 |
print(f"Error fetching questions: {e}")
|
| 63 |
return f"Error fetching questions: {e}", None
|
| 64 |
-
except requests.exceptions.JSONDecodeError as e:
|
| 65 |
-
print(f"Error decoding JSON response from questions endpoint: {e}")
|
| 66 |
-
print(f"Response text: {response.text[:500]}")
|
| 67 |
-
return f"Error decoding server response for questions: {e}", None
|
| 68 |
except Exception as e:
|
| 69 |
print(f"An unexpected error occurred fetching questions: {e}")
|
| 70 |
return f"An unexpected error occurred fetching questions: {e}", None
|
|
@@ -76,16 +133,27 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
|
|
| 76 |
for item in questions_data:
|
| 77 |
task_id = item.get("task_id")
|
| 78 |
question_text = item.get("question")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
if not task_id or question_text is None:
|
| 80 |
print(f"Skipping item with missing task_id or question: {item}")
|
| 81 |
continue
|
| 82 |
try:
|
| 83 |
-
|
|
|
|
|
|
|
| 84 |
answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
|
| 85 |
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
|
| 86 |
except Exception as e:
|
| 87 |
-
|
| 88 |
-
|
| 89 |
|
| 90 |
if not answers_payload:
|
| 91 |
print("Agent did not produce any answers to submit.")
|
|
@@ -140,6 +208,10 @@ def run_and_submit_all( profile: gr.OAuthProfile | None):
|
|
| 140 |
return status_message, results_df
|
| 141 |
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
# --- Build Gradio Interface using Blocks ---
|
| 144 |
with gr.Blocks() as demo:
|
| 145 |
gr.Markdown("# Basic Agent Evaluation Runner")
|
|
@@ -161,6 +233,7 @@ with gr.Blocks() as demo:
|
|
| 161 |
gr.LoginButton()
|
| 162 |
|
| 163 |
run_button = gr.Button("Run Evaluation & Submit All Answers")
|
|
|
|
| 164 |
|
| 165 |
status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
|
| 166 |
# Removed max_rows=10 from DataFrame constructor
|
|
@@ -170,6 +243,10 @@ with gr.Blocks() as demo:
|
|
| 170 |
fn=run_and_submit_all,
|
| 171 |
outputs=[status_output, results_table]
|
| 172 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
if __name__ == "__main__":
|
| 175 |
print("\n" + "-"*30 + " App Starting " + "-"*30)
|
|
|
|
| 3 |
import requests
|
| 4 |
import inspect
|
| 5 |
import pandas as pd
|
| 6 |
+
from agent import BasicAgent
|
| 7 |
|
| 8 |
# (Keep Constants as is)
|
| 9 |
# --- Constants ---
|
| 10 |
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
| 11 |
|
| 12 |
+
def run_and_print_random():
|
| 13 |
+
api_url = DEFAULT_API_URL
|
| 14 |
+
random_question_url = f"{api_url}/random-question"
|
| 15 |
+
submit_url = f"{api_url}/submit"
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
agent = BasicAgent(verbose=True)
|
| 19 |
+
except Exception as e:
|
| 20 |
+
print(f"Error instantiating agent: {e}")
|
| 21 |
+
return f"Error initializing agent: {e}", None
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
response = requests.get(random_question_url, timeout=15)
|
| 25 |
+
response.raise_for_status()
|
| 26 |
+
question_data = response.json()
|
| 27 |
+
if not question_data:
|
| 28 |
+
return "Fetched questions list is empty or invalid format.", None
|
| 29 |
+
print(f"Fetched {len(question_data)} questions.")
|
| 30 |
+
except requests.exceptions.JSONDecodeError as e:
|
| 31 |
+
return f"Error decoding server response for questions: {e}", None
|
| 32 |
+
except requests.exceptions.RequestException as e:
|
| 33 |
+
return f"Error fetching questions: {e}", None
|
| 34 |
+
except Exception as e:
|
| 35 |
+
return f"An unexpected error occurred fetching questions: {e}", None
|
| 36 |
+
|
| 37 |
+
results_log = []
|
| 38 |
+
answers_payload = []
|
| 39 |
+
|
| 40 |
+
task_id = question_data.get("task_id")
|
| 41 |
+
question_text = question_data.get("question")
|
| 42 |
+
if not task_id or question_text is None:
|
| 43 |
+
print(f"Skipping item with missing task_id or question: {question_data}")
|
| 44 |
+
try:
|
| 45 |
+
submitted_answer = agent.call_sync(question_text)
|
| 46 |
+
answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
|
| 47 |
+
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
|
| 48 |
+
except Exception as e:
|
| 49 |
+
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
|
| 50 |
+
|
| 51 |
+
if not answers_payload:
|
| 52 |
+
return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
|
| 53 |
+
|
| 54 |
+
# 4. Prepare Submission
|
| 55 |
+
submission_data = {"username": 'agsagds', "agent_code": 'https://huggingface.co/spaces/agsagds/Unit_3_Agentic_RAG/tree/main', "answers": answers_payload}
|
| 56 |
+
status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user agsagds..."
|
| 57 |
+
print(status_update)
|
| 58 |
|
| 59 |
+
# 5. Submit
|
| 60 |
+
print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
|
| 61 |
+
try:
|
| 62 |
+
response = requests.post(submit_url, json=submission_data, timeout=60)
|
| 63 |
+
response.raise_for_status()
|
| 64 |
+
result_data = response.json()
|
| 65 |
+
final_status = (
|
| 66 |
+
f"Submission Successful!\n"
|
| 67 |
+
f"User: {result_data.get('username')}\n"
|
| 68 |
+
f"Overall Score: {result_data.get('score', 'N/A')}% "
|
| 69 |
+
f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
|
| 70 |
+
f"Message: {result_data.get('message', 'No message received.')}"
|
| 71 |
+
)
|
| 72 |
+
print("Submission successful.")
|
| 73 |
+
results_log.append(final_status)
|
| 74 |
+
results_df = pd.DataFrame(results_log)
|
| 75 |
+
return results_df
|
| 76 |
+
except Exception as e:
|
| 77 |
+
return f"Error submitting answers: {e}", None
|
| 78 |
+
|
| 79 |
def run_and_submit_all( profile: gr.OAuthProfile | None):
|
| 80 |
"""
|
| 81 |
Fetches all questions, runs the BasicAgent on them, submits all answers,
|
|
|
|
| 97 |
|
| 98 |
# 1. Instantiate Agent ( modify this part to create your agent)
|
| 99 |
try:
|
| 100 |
+
agent = BasicAgent(verbose=True)
|
| 101 |
except Exception as e:
|
| 102 |
print(f"Error instantiating agent: {e}")
|
| 103 |
return f"Error initializing agent: {e}", None
|
|
|
|
| 112 |
response.raise_for_status()
|
| 113 |
questions_data = response.json()
|
| 114 |
if not questions_data:
|
| 115 |
+
print("Fetched questions list is empty.")
|
| 116 |
+
return "Fetched questions list is empty or invalid format.", None
|
| 117 |
print(f"Fetched {len(questions_data)} questions.")
|
| 118 |
+
except requests.exceptions.JSONDecodeError as e:
|
| 119 |
+
print(f"Error decoding JSON response from questions endpoint: {e}")
|
| 120 |
+
print(f"Response text: {response.text[:500]}")
|
| 121 |
+
return f"Error decoding server response for questions: {e}", None
|
| 122 |
except requests.exceptions.RequestException as e:
|
| 123 |
print(f"Error fetching questions: {e}")
|
| 124 |
return f"Error fetching questions: {e}", None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
except Exception as e:
|
| 126 |
print(f"An unexpected error occurred fetching questions: {e}")
|
| 127 |
return f"An unexpected error occurred fetching questions: {e}", None
|
|
|
|
| 133 |
for item in questions_data:
|
| 134 |
task_id = item.get("task_id")
|
| 135 |
question_text = item.get("question")
|
| 136 |
+
file_name = item.get("file_name")
|
| 137 |
+
|
| 138 |
+
if file_name:
|
| 139 |
+
print(f"Skipping item with file name: {file_name} because it is not supported yet.")
|
| 140 |
+
continue
|
| 141 |
+
if "youtube" in question_text.lower():
|
| 142 |
+
print(f"Skipping item with youtube link: {question_text} because it is not supported yet.")
|
| 143 |
+
continue
|
| 144 |
+
|
| 145 |
if not task_id or question_text is None:
|
| 146 |
print(f"Skipping item with missing task_id or question: {item}")
|
| 147 |
continue
|
| 148 |
try:
|
| 149 |
+
print(f"Running agent on question: {task_id} {question_text}")
|
| 150 |
+
submitted_answer = agent.call_sync(question_text)
|
| 151 |
+
print(f"Submitted answer: {submitted_answer}")
|
| 152 |
answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
|
| 153 |
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
|
| 154 |
except Exception as e:
|
| 155 |
+
print(f"Error running agent on task {task_id}: {e}")
|
| 156 |
+
results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
|
| 157 |
|
| 158 |
if not answers_payload:
|
| 159 |
print("Agent did not produce any answers to submit.")
|
|
|
|
| 208 |
return status_message, results_df
|
| 209 |
|
| 210 |
|
| 211 |
+
# Load evns from .env file
|
| 212 |
+
from dotenv import load_dotenv
|
| 213 |
+
load_dotenv()
|
| 214 |
+
|
| 215 |
# --- Build Gradio Interface using Blocks ---
|
| 216 |
with gr.Blocks() as demo:
|
| 217 |
gr.Markdown("# Basic Agent Evaluation Runner")
|
|
|
|
| 233 |
gr.LoginButton()
|
| 234 |
|
| 235 |
run_button = gr.Button("Run Evaluation & Submit All Answers")
|
| 236 |
+
run_once_button = gr.Button("Run Evaluation for random question")
|
| 237 |
|
| 238 |
status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
|
| 239 |
# Removed max_rows=10 from DataFrame constructor
|
|
|
|
| 243 |
fn=run_and_submit_all,
|
| 244 |
outputs=[status_output, results_table]
|
| 245 |
)
|
| 246 |
+
run_once_button.click(
|
| 247 |
+
fn=run_and_print_random,
|
| 248 |
+
outputs=[results_table]
|
| 249 |
+
)
|
| 250 |
|
| 251 |
if __name__ == "__main__":
|
| 252 |
print("\n" + "-"*30 + " App Starting " + "-"*30)
|
requirements.txt
CHANGED
|
@@ -1,2 +1,145 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiofiles==24.1.0
|
| 2 |
+
aiohappyeyeballs==2.6.1
|
| 3 |
+
aiohttp==3.13.2
|
| 4 |
+
aiosignal==1.4.0
|
| 5 |
+
aiosqlite==0.21.0
|
| 6 |
+
annotated-doc==0.0.3
|
| 7 |
+
annotated-types==0.7.0
|
| 8 |
+
anyio==4.11.0
|
| 9 |
+
arxiv==2.3.0
|
| 10 |
+
attrs==25.4.0
|
| 11 |
+
Authlib==1.6.5
|
| 12 |
+
banks==2.2.0
|
| 13 |
+
beautifulsoup4==4.14.2
|
| 14 |
+
Brotli==1.1.0
|
| 15 |
+
certifi==2025.10.5
|
| 16 |
+
cffi==2.0.0
|
| 17 |
+
chardet==5.2.0
|
| 18 |
+
charset-normalizer==3.4.4
|
| 19 |
+
click==8.3.0
|
| 20 |
+
colorama==0.4.6
|
| 21 |
+
cryptography==46.0.3
|
| 22 |
+
cssselect==1.3.0
|
| 23 |
+
dataclasses-json==0.6.7
|
| 24 |
+
ddgs==9.9.0
|
| 25 |
+
defusedxml==0.7.1
|
| 26 |
+
Deprecated==1.2.18
|
| 27 |
+
dirtyjson==1.0.8
|
| 28 |
+
distro==1.9.0
|
| 29 |
+
duckduckgo_search==6.4.2
|
| 30 |
+
fastapi==0.121.0
|
| 31 |
+
feedparser==6.0.12
|
| 32 |
+
ffmpy==0.6.4
|
| 33 |
+
filelock==3.20.0
|
| 34 |
+
filetype==1.2.0
|
| 35 |
+
frozenlist==1.8.0
|
| 36 |
+
fsspec==2025.10.0
|
| 37 |
+
gradio==5.49.1
|
| 38 |
+
gradio_client==1.13.3
|
| 39 |
+
greenlet==3.2.4
|
| 40 |
+
griffe==1.14.0
|
| 41 |
+
groovy==0.1.2
|
| 42 |
+
h11==0.16.0
|
| 43 |
+
h2==4.3.0
|
| 44 |
+
hf-xet==1.2.0
|
| 45 |
+
hpack==4.1.0
|
| 46 |
+
html5lib==1.1
|
| 47 |
+
httpcore==1.0.9
|
| 48 |
+
httpx==0.28.1
|
| 49 |
+
huggingface-hub==0.36.0
|
| 50 |
+
hyperframe==6.1.0
|
| 51 |
+
idna==3.11
|
| 52 |
+
itsdangerous==2.2.0
|
| 53 |
+
Jinja2==3.1.6
|
| 54 |
+
jiter==0.11.1
|
| 55 |
+
joblib==1.5.2
|
| 56 |
+
llama-cloud==0.1.35
|
| 57 |
+
llama-cloud-services==0.6.54
|
| 58 |
+
llama-index==0.14.8
|
| 59 |
+
llama-index-cli==0.5.3
|
| 60 |
+
llama-index-core==0.14.8
|
| 61 |
+
llama-index-embeddings-openai==0.5.1
|
| 62 |
+
llama-index-indices-managed-llama-cloud==0.9.4
|
| 63 |
+
llama-index-instrumentation==0.4.2
|
| 64 |
+
llama-index-llms-huggingface-api==0.6.1
|
| 65 |
+
llama-index-llms-nebius==0.2.1
|
| 66 |
+
llama-index-llms-ollama==0.9.0
|
| 67 |
+
llama-index-llms-openai==0.6.7
|
| 68 |
+
llama-index-llms-openai-like==0.5.3
|
| 69 |
+
llama-index-readers-file==0.5.4
|
| 70 |
+
llama-index-readers-llama-parse==0.5.1
|
| 71 |
+
llama-index-tools-arxiv==0.4.1
|
| 72 |
+
llama-index-tools-duckduckgo==0.4.1
|
| 73 |
+
llama-index-tools-wikipedia==0.4.1
|
| 74 |
+
llama-index-workflows==2.10.3
|
| 75 |
+
llama-parse==0.6.54
|
| 76 |
+
lxml==6.0.2
|
| 77 |
+
lxml_html_clean==0.4.3
|
| 78 |
+
markdown-it-py==4.0.0
|
| 79 |
+
MarkupSafe==3.0.3
|
| 80 |
+
marshmallow==3.26.1
|
| 81 |
+
mdurl==0.1.2
|
| 82 |
+
multidict==6.7.0
|
| 83 |
+
mypy_extensions==1.1.0
|
| 84 |
+
nest-asyncio==1.6.0
|
| 85 |
+
networkx==3.5
|
| 86 |
+
nltk==3.9.2
|
| 87 |
+
numpy==2.3.4
|
| 88 |
+
ollama==0.6.0
|
| 89 |
+
openai==1.109.1
|
| 90 |
+
orjson==3.11.4
|
| 91 |
+
packaging==25.0
|
| 92 |
+
pandas==2.2.3
|
| 93 |
+
pillow==11.3.0
|
| 94 |
+
platformdirs==4.5.0
|
| 95 |
+
primp==0.15.0
|
| 96 |
+
propcache==0.4.1
|
| 97 |
+
pycparser==2.23
|
| 98 |
+
pydantic==2.11.10
|
| 99 |
+
pydantic_core==2.33.2
|
| 100 |
+
pydub==0.25.1
|
| 101 |
+
Pygments==2.19.2
|
| 102 |
+
pypdf==6.2.0
|
| 103 |
+
python-dateutil==2.9.0.post0
|
| 104 |
+
python-dotenv==1.2.1
|
| 105 |
+
python-multipart==0.0.20
|
| 106 |
+
pytz==2025.2
|
| 107 |
+
PyYAML==6.0.3
|
| 108 |
+
readabilipy==0.3.0
|
| 109 |
+
readability-lxml==0.8.4.1
|
| 110 |
+
regex==2025.11.3
|
| 111 |
+
requests==2.32.5
|
| 112 |
+
rich==14.2.0
|
| 113 |
+
ruff==0.14.3
|
| 114 |
+
safehttpx==0.1.7
|
| 115 |
+
safetensors==0.6.2
|
| 116 |
+
semantic-version==2.10.0
|
| 117 |
+
setuptools==80.9.0
|
| 118 |
+
sgmllib3k==1.0.0
|
| 119 |
+
shellingham==1.5.4
|
| 120 |
+
six==1.17.0
|
| 121 |
+
sniffio==1.3.1
|
| 122 |
+
socksio==1.0.0
|
| 123 |
+
soupsieve==2.8
|
| 124 |
+
SQLAlchemy==2.0.44
|
| 125 |
+
starlette==0.49.3
|
| 126 |
+
striprtf==0.0.26
|
| 127 |
+
tenacity==9.1.2
|
| 128 |
+
tiktoken==0.12.0
|
| 129 |
+
tokenizers==0.22.1
|
| 130 |
+
tomlkit==0.13.3
|
| 131 |
+
tqdm==4.67.1
|
| 132 |
+
transformers==4.57.1
|
| 133 |
+
typer==0.20.0
|
| 134 |
+
typer-slim==0.20.0
|
| 135 |
+
typing-inspect==0.9.0
|
| 136 |
+
typing-inspection==0.4.2
|
| 137 |
+
typing_extensions==4.15.0
|
| 138 |
+
tzdata==2025.2
|
| 139 |
+
urllib3==2.5.0
|
| 140 |
+
uvicorn==0.38.0
|
| 141 |
+
webencodings==0.5.1
|
| 142 |
+
websockets==15.0.1
|
| 143 |
+
wikipedia==1.4.0
|
| 144 |
+
wrapt==1.17.3
|
| 145 |
+
yarl==1.22.0
|
tools.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import os
|
| 3 |
+
import mimetypes
|
| 4 |
+
import httpx
|
| 5 |
+
from llama_index.core.llms import ChatMessage, TextBlock, ImageBlock
|
| 6 |
+
from llama_index.llms.nebius import NebiusLLM
|
| 7 |
+
from ddgs import DDGS
|
| 8 |
+
from ddgs.exceptions import DDGSException
|
| 9 |
+
import bs4
|
| 10 |
+
from readability import Document
|
| 11 |
+
|
| 12 |
+
def multiply(a: float, b: float) -> float:
|
| 13 |
+
"""Multiply two numbers and returns the product"""
|
| 14 |
+
return a * b
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def add(a: float, b: float) -> float:
|
| 18 |
+
"""Add two numbers and returns the sum"""
|
| 19 |
+
return a + b
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def webSearchTool(
|
| 23 |
+
query: str,
|
| 24 |
+
region: str = "us-en",
|
| 25 |
+
timelimit: str | None = None
|
| 26 |
+
) -> list[dict[str, str]]:
|
| 27 |
+
"""
|
| 28 |
+
Perform a web search using DuckDuckGo metasearch across multiple backends.
|
| 29 |
+
|
| 30 |
+
This tool searches the web using DuckDuckGo's metasearch engine, which can query
|
| 31 |
+
multiple search backends including Bing, Brave, DuckDuckGo, Google, Mojeek,
|
| 32 |
+
Yandex, Yahoo, and Wikipedia. Returns a list of search results with titles,
|
| 33 |
+
snippets, and URLs.
|
| 34 |
+
|
| 35 |
+
Args:
|
| 36 |
+
query: The search query text. Supports advanced search operators:
|
| 37 |
+
- "exact phrase" - Search for exact phrase
|
| 38 |
+
- term1 -term2 - Exclude term2 from results
|
| 39 |
+
- term1 +term2 - Emphasize term2 in results
|
| 40 |
+
- term filetype:pdf - Search for specific file types (pdf, doc, docx, xls, xlsx, ppt, pptx, html)
|
| 41 |
+
- term site:example.com - Search within a specific site
|
| 42 |
+
- term -site:example.com - Exclude a specific site
|
| 43 |
+
- intitle:term - Search in page titles
|
| 44 |
+
- inurl:term - Search in page URLs
|
| 45 |
+
|
| 46 |
+
region: Search region/locale. Examples: "us-en", "uk-en", "ru-ru", etc. Defaults to "us-en".
|
| 47 |
+
|
| 48 |
+
timelimit: Limit results to a specific time period. Options: "d" (day), "w" (week),
|
| 49 |
+
"m" (month), "y" (year). Defaults to None (no time limit).
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
A list of dictionaries, where each dictionary contains search result information
|
| 53 |
+
with keys such as 'title', 'body', 'href', etc.
|
| 54 |
+
|
| 55 |
+
Example:
|
| 56 |
+
>>> results = webSearchTool("Python programming")
|
| 57 |
+
>>> results = webSearchTool("machine learning filetype:pdf")
|
| 58 |
+
>>> results = webSearchTool("news site:example.com")
|
| 59 |
+
"""
|
| 60 |
+
try:
|
| 61 |
+
return list(DDGS().text(
|
| 62 |
+
query=query,
|
| 63 |
+
region=region,
|
| 64 |
+
timelimit=timelimit
|
| 65 |
+
))
|
| 66 |
+
except DDGSException:
|
| 67 |
+
# If no results found, return empty list
|
| 68 |
+
return []
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
async def directFetchTool(url: str, offset: int = 0) -> str:
|
| 72 |
+
"""
|
| 73 |
+
Fetch and extract only the meaningful readable content from a webpage,
|
| 74 |
+
similar to Chrome/Firefox Reader Mode. Removes navigation, ads, sidebars,
|
| 75 |
+
comments, and other non-essential content, keeping only the main article text.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
url: The URL of the webpage to fetch. Must be a valid HTTP or HTTPS URL.
|
| 79 |
+
offset: position from start of the web page content. Default = 0
|
| 80 |
+
Returns:
|
| 81 |
+
The extracted meaningful text content of the webpage as a string.
|
| 82 |
+
If result length more then 2000 symbols than return only first 2000. Use `offset` option to show another part of the web page content.
|
| 83 |
+
If an error occurs, returns an empty string.
|
| 84 |
+
|
| 85 |
+
Example:
|
| 86 |
+
>>> content = await direct_fetch_tool("https://example.com/article")
|
| 87 |
+
>>> # Use after webSearchTool to get full page content
|
| 88 |
+
>>> results = webSearchTool("Python tutorial")
|
| 89 |
+
>>> if results:
|
| 90 |
+
>>> first_url = results[0].get('href')
|
| 91 |
+
>>> full_content = await direct_fetch_tool(first_url)
|
| 92 |
+
"""
|
| 93 |
+
try:
|
| 94 |
+
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
| 95 |
+
response = await client.get(url, headers={
|
| 96 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
| 97 |
+
})
|
| 98 |
+
response.raise_for_status()
|
| 99 |
+
html = response.text
|
| 100 |
+
|
| 101 |
+
# Use readability-lxml to extract meaningful content (like Reader Mode)
|
| 102 |
+
# This uses Mozilla's Readability algorithm
|
| 103 |
+
doc = Document(html)
|
| 104 |
+
readable_html = doc.summary()
|
| 105 |
+
|
| 106 |
+
# Parse the cleaned HTML and extract text
|
| 107 |
+
soup = bs4.BeautifulSoup(readable_html, "html.parser")
|
| 108 |
+
|
| 109 |
+
# Remove any remaining script and style elements
|
| 110 |
+
for elem in soup(['script', 'style', 'nav', 'header', 'footer', 'aside']):
|
| 111 |
+
elem.decompose()
|
| 112 |
+
|
| 113 |
+
# Extract text with proper formatting
|
| 114 |
+
text = soup.get_text(separator='\n', strip=True)
|
| 115 |
+
|
| 116 |
+
# Clean up excessive whitespace while preserving paragraph breaks
|
| 117 |
+
lines = [line.strip() for line in text.split('\n') if line.strip()]
|
| 118 |
+
text = '\n'.join(lines)
|
| 119 |
+
|
| 120 |
+
if len(text)>2000:
|
| 121 |
+
return f"Result is too big: {len(text)} chars. Return only slice ${offset}:${offset+2000}: \n\n" + text[offset:offset+2000]
|
| 122 |
+
|
| 123 |
+
return text
|
| 124 |
+
except httpx.HTTPStatusError as e:
|
| 125 |
+
error_msg = f"Error fetching webpage (HTTP {e.response.status_code}): {url}"
|
| 126 |
+
print(error_msg)
|
| 127 |
+
return ""
|
| 128 |
+
except httpx.TimeoutException:
|
| 129 |
+
error_msg = f"Timeout while fetching webpage: {url}"
|
| 130 |
+
print(error_msg)
|
| 131 |
+
return ""
|
| 132 |
+
except httpx.RequestError as e:
|
| 133 |
+
error_msg = f"Error fetching webpage: {str(e)}"
|
| 134 |
+
print(error_msg)
|
| 135 |
+
return ""
|
| 136 |
+
except Exception as e:
|
| 137 |
+
error_msg = f"Unexpected error fetching webpage: {str(e)}"
|
| 138 |
+
print(error_msg)
|
| 139 |
+
return ""
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
async def describeImage(imgUrl: str, instructions: str = "Describe the image.") -> str:
|
| 143 |
+
"""
|
| 144 |
+
Describe an image using a image-to-text model.
|
| 145 |
+
"""
|
| 146 |
+
vision_llm = NebiusLLM(
|
| 147 |
+
api_key=os.getenv("NEBIUS_API_KEY"),
|
| 148 |
+
model="nvidia/Nemotron-Nano-V2-12b",
|
| 149 |
+
api_base="https://api.tokenfactory.nebius.com/v1"
|
| 150 |
+
)
|
| 151 |
+
try:
|
| 152 |
+
messages = [
|
| 153 |
+
ChatMessage(
|
| 154 |
+
role="user",
|
| 155 |
+
blocks=[
|
| 156 |
+
TextBlock(text=instructions),
|
| 157 |
+
ImageBlock(url=imgUrl),
|
| 158 |
+
],
|
| 159 |
+
),
|
| 160 |
+
]
|
| 161 |
+
|
| 162 |
+
# response = vision_llm.stream_chat(messages)
|
| 163 |
+
# for r in response:
|
| 164 |
+
# print(r.delta, end="")
|
| 165 |
+
response = await vision_llm.achat(messages)
|
| 166 |
+
|
| 167 |
+
return str(response.message).split("</think>")[-1].strip()
|
| 168 |
+
except Exception as e:
|
| 169 |
+
error_msg = f"Error extracting text: {str(e)}"
|
| 170 |
+
print(error_msg)
|
| 171 |
+
return ""
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
async def transcribeAudio(audioUrlOrPath: str, language_code: str = None) -> str:
|
| 175 |
+
"""
|
| 176 |
+
Transcribe an audio or video file using ElevenLabs speech-to-text API.
|
| 177 |
+
|
| 178 |
+
Args:
|
| 179 |
+
audioUrlOrPath: URL or local file path to the audio/video file
|
| 180 |
+
language_code: Optional language code (e.g., 'en', 'es', 'fr')
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
The transcribed text as a string
|
| 184 |
+
"""
|
| 185 |
+
api_key = os.getenv("ELEVENLABS_STT_API_KEY")
|
| 186 |
+
if not api_key:
|
| 187 |
+
error_msg = "Error: ELEVENLABS_STT_API_KEY not found in environment variables"
|
| 188 |
+
print(error_msg)
|
| 189 |
+
return ""
|
| 190 |
+
|
| 191 |
+
try:
|
| 192 |
+
# Determine if input is a URL or file path
|
| 193 |
+
is_url = audioUrlOrPath.startswith(('http://', 'https://'))
|
| 194 |
+
|
| 195 |
+
# Prepare the audio file
|
| 196 |
+
if is_url:
|
| 197 |
+
# Download the file from URL
|
| 198 |
+
async with httpx.AsyncClient() as client:
|
| 199 |
+
response = await client.get(audioUrlOrPath)
|
| 200 |
+
response.raise_for_status()
|
| 201 |
+
audio_data = response.content
|
| 202 |
+
# Try to get filename from URL or Content-Disposition header
|
| 203 |
+
filename = audioUrlOrPath.split('/')[-1].split('?')[0] or 'audio_file'
|
| 204 |
+
else:
|
| 205 |
+
# Read from local file path
|
| 206 |
+
with open(audioUrlOrPath, 'rb') as f:
|
| 207 |
+
audio_data = f.read()
|
| 208 |
+
filename = os.path.basename(audioUrlOrPath)
|
| 209 |
+
|
| 210 |
+
# Detect MIME type from filename extension, fallback to octet-stream
|
| 211 |
+
content_type, _ = mimetypes.guess_type(filename)
|
| 212 |
+
if not content_type:
|
| 213 |
+
content_type = 'application/octet-stream'
|
| 214 |
+
|
| 215 |
+
# Prepare multipart form data
|
| 216 |
+
files = {
|
| 217 |
+
'file': (filename, audio_data, content_type)
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
data = { 'model_id': 'scribe_v1' }
|
| 221 |
+
if language_code:
|
| 222 |
+
data['language_code'] = language_code
|
| 223 |
+
|
| 224 |
+
# Make the API request
|
| 225 |
+
async with httpx.AsyncClient() as client:
|
| 226 |
+
response = await client.post(
|
| 227 |
+
'https://api.elevenlabs.io/v1/speech-to-text',
|
| 228 |
+
headers={
|
| 229 |
+
'xi-api-key': api_key
|
| 230 |
+
},
|
| 231 |
+
files=files,
|
| 232 |
+
data=data,
|
| 233 |
+
timeout=300.0 # 5 minutes timeout for large files
|
| 234 |
+
)
|
| 235 |
+
response.raise_for_status()
|
| 236 |
+
result = response.json()
|
| 237 |
+
|
| 238 |
+
# Extract transcript from response
|
| 239 |
+
if 'text' in result:
|
| 240 |
+
return result['text']
|
| 241 |
+
else:
|
| 242 |
+
# Fallback: return the entire response as string
|
| 243 |
+
return str(result)
|
| 244 |
+
|
| 245 |
+
except httpx.HTTPStatusError as e:
|
| 246 |
+
error_msg = f"Error transcribing audio (HTTP {e.response.status_code}): {e.response.text}"
|
| 247 |
+
print(error_msg)
|
| 248 |
+
return ""
|
| 249 |
+
except Exception as e:
|
| 250 |
+
error_msg = f"Error transcribing audio: {str(e)}"
|
| 251 |
+
print(error_msg)
|
| 252 |
+
return ""
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
if __name__ == "__main__":
|
| 256 |
+
from dotenv import load_dotenv
|
| 257 |
+
load_dotenv()
|
| 258 |
+
|
| 259 |
+
# print(extract_shape("https://developers.llamaindex.ai/python/_astro/llamaindex-light.BJap_D_H.svg"))
|
| 260 |
+
|
| 261 |
+
async def main():
|
| 262 |
+
|
| 263 |
+
url = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fstpagmaster.blob.core.windows.net%2Fcontainer-queensgambitaccepted-jpeg%2Fintro.png&f=1&nofb=1&ipt=1c348904c4fe4508d241e5527be8203e4cc2c029ed7e0cdeba3bbf372ab30a96"
|
| 264 |
+
print(await describeImage(url))
|
| 265 |
+
|
| 266 |
+
# url = 'https://www.voiptroubleshooter.com/open_speech/american/OSR_us_000_0011_8k.wav'
|
| 267 |
+
# print(await transcribeAudio(url, 'en'))
|
| 268 |
+
# results = webSearchTool("Diplodocus nominated FA 2016")
|
| 269 |
+
# print(results)
|
| 270 |
+
# wp = await directFetchTool('https://en.wikipedia.org/wiki/Capital_of_France')
|
| 271 |
+
# print(wp[:3000])
|
| 272 |
+
# print('\n\n', len(wp))
|
| 273 |
+
asyncio.run(main())
|
| 274 |
+
|