agsagds commited on
Commit
dec6bf3
·
1 Parent(s): a45f1cd

feat: implement tools and agentic behaviour

Browse files
Files changed (5) hide show
  1. __init__.py +0 -0
  2. agent.py +130 -0
  3. app.py +97 -20
  4. requirements.txt +145 -2
  5. 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
- # --- Basic Agent Definition ---
12
- # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
13
- class BasicAgent:
14
- def __init__(self):
15
- print("BasicAgent initialized.")
16
- def __call__(self, question: str) -> str:
17
- print(f"Agent received question (first 50 chars): {question[:50]}...")
18
- fixed_answer = "This is a default answer."
19
- print(f"Agent returning fixed answer: {fixed_answer}")
20
- return fixed_answer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- print("Fetched questions list is empty.")
59
- return "Fetched questions list is empty or invalid format.", None
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
- submitted_answer = agent(question_text)
 
 
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
- print(f"Error running agent on task {task_id}: {e}")
88
- results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
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
- gradio
2
- requests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+