Snaseem2026 commited on
Commit
72f4b5c
·
verified ·
1 Parent(s): b2efdb5

Upload 9 files

Browse files
Files changed (9) hide show
  1. README.md +2 -2
  2. agent.py +208 -0
  3. app.py +168 -215
  4. image_processing.py +26 -0
  5. metadata.jsonl +0 -0
  6. requirements.txt +19 -6
  7. supabase_docs.csv +0 -0
  8. system_prompt.txt +5 -0
  9. tools.py +564 -0
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🕵🏻‍♂️
4
  colorFrom: indigo
5
  colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 5.25.2
8
  app_file: app.py
9
  pinned: false
10
  hf_oauth: true
@@ -12,4 +12,4 @@ hf_oauth: true
12
  hf_oauth_expiration_minutes: 480
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
4
  colorFrom: indigo
5
  colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.35.0
8
  app_file: app.py
9
  pinned: false
10
  hf_oauth: true
 
12
  hf_oauth_expiration_minutes: 480
13
  ---
14
 
15
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
agent.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LangGraph Agent"""
2
+ import os
3
+ from dotenv import load_dotenv
4
+ from langgraph.graph import START, StateGraph, MessagesState
5
+ from langgraph.prebuilt import tools_condition
6
+ from langgraph.prebuilt import ToolNode
7
+ from langchain_google_genai import ChatGoogleGenerativeAI
8
+ from langchain_groq import ChatGroq
9
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint, HuggingFaceEmbeddings
10
+ from langchain_community.tools.tavily_search import TavilySearchResults
11
+ from langchain_community.document_loaders import WikipediaLoader
12
+ from langchain_community.document_loaders import ArxivLoader
13
+ from langchain_community.vectorstores import SupabaseVectorStore
14
+ from langchain_core.messages import SystemMessage, HumanMessage
15
+ from langchain_core.tools import tool
16
+ from langchain.tools.retriever import create_retriever_tool
17
+ from supabase.client import Client, create_client
18
+
19
+ load_dotenv()
20
+
21
+ @tool
22
+ def multiply(a: int, b: int) -> int:
23
+ """Multiply two numbers.
24
+
25
+ Args:
26
+ a: first int
27
+ b: second int
28
+ """
29
+ return a * b
30
+
31
+ @tool
32
+ def add(a: int, b: int) -> int:
33
+ """Add two numbers.
34
+
35
+ Args:
36
+ a: first int
37
+ b: second int
38
+ """
39
+ return a + b
40
+
41
+ @tool
42
+ def subtract(a: int, b: int) -> int:
43
+ """Subtract two numbers.
44
+
45
+ Args:
46
+ a: first int
47
+ b: second int
48
+ """
49
+ return a - b
50
+
51
+ @tool
52
+ def divide(a: int, b: int) -> int:
53
+ """Divide two numbers.
54
+
55
+ Args:
56
+ a: first int
57
+ b: second int
58
+ """
59
+ if b == 0:
60
+ raise ValueError("Cannot divide by zero.")
61
+ return a / b
62
+
63
+ @tool
64
+ def modulus(a: int, b: int) -> int:
65
+ """Get the modulus of two numbers.
66
+
67
+ Args:
68
+ a: first int
69
+ b: second int
70
+ """
71
+ return a % b
72
+
73
+ @tool
74
+ def wiki_search(query: str) -> str:
75
+ """Search Wikipedia for a query and return maximum 2 results.
76
+
77
+ Args:
78
+ query: The search query."""
79
+ search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
80
+ formatted_search_docs = "\n\n---\n\n".join(
81
+ [
82
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
83
+ for doc in search_docs
84
+ ])
85
+ return {"wiki_results": formatted_search_docs}
86
+
87
+ @tool
88
+ def web_search(query: str) -> str:
89
+ """Search Tavily for a query and return maximum 3 results.
90
+
91
+ Args:
92
+ query: The search query."""
93
+ search_docs = TavilySearchResults(max_results=3).invoke(query=query)
94
+ formatted_search_docs = "\n\n---\n\n".join(
95
+ [
96
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
97
+ for doc in search_docs
98
+ ])
99
+ return {"web_results": formatted_search_docs}
100
+
101
+ @tool
102
+ def arvix_search(query: str) -> str:
103
+ """Search Arxiv for a query and return maximum 3 result.
104
+
105
+ Args:
106
+ query: The search query."""
107
+ search_docs = ArxivLoader(query=query, load_max_docs=3).load()
108
+ formatted_search_docs = "\n\n---\n\n".join(
109
+ [
110
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
111
+ for doc in search_docs
112
+ ])
113
+ return {"arvix_results": formatted_search_docs}
114
+
115
+
116
+
117
+ # load the system prompt from the file
118
+ with open("system_prompt.txt", "r", encoding="utf-8") as f:
119
+ system_prompt = f.read()
120
+
121
+ # System message
122
+ sys_msg = SystemMessage(content=system_prompt)
123
+
124
+ # build a retriever
125
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2") # dim=768
126
+ supabase: Client = create_client(
127
+ os.environ.get("SUPABASE_URL"),
128
+ os.environ.get("SUPABASE_SERVICE_KEY"))
129
+ vector_store = SupabaseVectorStore(
130
+ client=supabase,
131
+ embedding= embeddings,
132
+ table_name="documents2",
133
+ query_name="match_documents_2",
134
+ )
135
+ create_retriever_tool = create_retriever_tool(
136
+ retriever=vector_store.as_retriever(),
137
+ name="Question Search",
138
+ description="A tool to retrieve similar questions from a vector store.",
139
+ )
140
+
141
+
142
+
143
+ tools = [
144
+ multiply,
145
+ add,
146
+ subtract,
147
+ divide,
148
+ modulus,
149
+ wiki_search,
150
+ web_search,
151
+ arvix_search,
152
+ ]
153
+
154
+ # Build graph function
155
+ def build_graph(provider: str = "huggingface"):
156
+ """Build the graph"""
157
+
158
+ if provider == "groq":
159
+ llm = ChatGroq(model="qwen-qwq-32b", temperature=0) # optional : qwen-qwq-32b gemma2-9b-it
160
+ elif provider == "huggingface":
161
+ llm = ChatHuggingFace(
162
+ llm=HuggingFaceEndpoint(
163
+ repo_id = "Qwen/Qwen2.5-Coder-32B-Instruct"
164
+ ),
165
+ )
166
+ else:
167
+ raise ValueError("Invalid provider. Choose 'google', 'groq' or 'huggingface'.")
168
+ # Bind tools to LLM
169
+ llm_with_tools = llm.bind_tools(tools)
170
+
171
+ # Node
172
+ def assistant(state: MessagesState):
173
+ """Assistant node"""
174
+ return {"messages": [llm_with_tools.invoke(state["messages"])]}
175
+
176
+ def retriever(state: MessagesState):
177
+ """Retriever node"""
178
+ similar_question = vector_store.similarity_search(state["messages"][0].content)
179
+ example_msg = HumanMessage(
180
+ content=f"Here I provide a similar question and answer for reference: \n\n{similar_question[0].page_content}",
181
+ )
182
+ return {"messages": [sys_msg] + state["messages"] + [example_msg]}
183
+
184
+ builder = StateGraph(MessagesState)
185
+ builder.add_node("retriever", retriever)
186
+ builder.add_node("assistant", assistant)
187
+ builder.add_node("tools", ToolNode(tools))
188
+ builder.add_edge(START, "retriever")
189
+ builder.add_edge("retriever", "assistant")
190
+ builder.add_conditional_edges(
191
+ "assistant",
192
+ tools_condition,
193
+ )
194
+ builder.add_edge("tools", "assistant")
195
+
196
+ # Compile graph
197
+ return builder.compile()
198
+
199
+ # test
200
+ if __name__ == "__main__":
201
+ question = "When was a picture of St. Thomas Aquinas first added to the Wikipedia page on the Principle of double effect?"
202
+ # Build the graph
203
+ graph = build_graph(provider="groq")
204
+ # Run the graph
205
+ messages = [HumanMessage(content=question)]
206
+ messages = graph.invoke({"messages": messages})
207
+ for m in messages["messages"]:
208
+ m.pretty_print()
app.py CHANGED
@@ -1,258 +1,211 @@
1
  import os
2
  import gradio as gr
3
  import requests
 
4
  import pandas as pd
5
- from huggingface_hub import InferenceClient
6
- import re
7
-
8
  # --- Constants ---
9
  DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
10
 
11
- # --- Reliable Agent ---
12
- class ReliableAgent:
 
13
  def __init__(self):
14
- print("Initializing Reliable Agent...")
15
-
16
- hf_token = os.getenv("HF_TOKEN")
17
- self.client = InferenceClient(token=hf_token)
18
-
19
- # Use fast, reliable model
20
- self.model = "mistralai/Mixtral-8x7B-Instruct-v0.1"
21
- print(f"✅ Model: {self.model}")
22
-
23
- # Initialize search
24
- try:
25
- from duckduckgo_search import DDGS
26
- self.search = DDGS()
27
- print("✅ Search ready")
28
- except:
29
- self.search = None
30
- print("⚠️ Search unavailable")
31
-
32
- def search_web(self, query: str) -> str:
33
- """Search and return concise results"""
34
- if not self.search:
35
- return ""
36
-
37
- try:
38
- results = list(self.search.text(query, max_results=5))
39
- if not results:
40
- return ""
41
-
42
- info = []
43
- for r in results[:5]:
44
- title = r.get('title', '')
45
- body = r.get('body', '')
46
- if title and body:
47
- info.append(f"{title}: {body}")
48
-
49
- return " | ".join(info)
50
- except:
51
- return ""
52
-
53
- def clean_answer(self, text: str) -> str:
54
- """Clean and extract answer"""
55
- # Remove common prefixes
56
- text = text.strip()
57
-
58
- # Remove verbose patterns
59
- patterns = [
60
- r'^(according to|based on|the answer is|answer is|answer:)\s*',
61
- r'^(therefore|thus|so|hence),?\s*',
62
- ]
63
-
64
- for pattern in patterns:
65
- text = re. sub(pattern, '', text, flags=re.IGNORECASE)
66
-
67
- # If multi-line, prefer shorter lines
68
- lines = [l.strip() for l in text.split('\n') if l.strip()]
69
-
70
- # Find best answer line
71
- for line in lines:
72
- # Good answer: 5-200 chars, doesn't end with ':'
73
- if 5 < len(line) < 200 and not line.endswith(': '):
74
- return line
75
-
76
- # Return first line if nothing better
77
- if lines:
78
- return lines[0][: 300]
79
-
80
- return text[: 300]
81
-
82
  def __call__(self, question: str) -> str:
83
- print(f"\nQ: {question[: 100]}")
84
-
85
- # Search if needed
86
- search_info = ""
87
- if self.search and any(kw in question. lower() for kw in ['who', 'what', 'where', 'when', 'current', 'latest', '2024', '2025', '2026']):
88
- search_info = self.search_web(question)
89
- if search_info:
90
- print(f" Found search info: {len(search_info)} chars")
91
-
92
- # Build concise prompt
93
- prompt = f"""Answer this question directly and concisely. Give ONLY the answer, no explanation.
94
 
95
- Question: {question}"""
96
-
97
- if search_info:
98
- prompt += f"\n\nRelevant information: {search_info[: 2000]}"
99
-
100
- prompt += "\n\nDirect answer:"
101
-
102
- # Call model
103
- try:
104
- response = self.client.text_generation(
105
- prompt,
106
- model=self.model,
107
- max_new_tokens=150,
108
- temperature=0.1,
109
- do_sample=False,
110
- stop_sequences=["\n\n", "Question:", "Q:"]
111
- )
112
-
113
- answer = self.clean_answer(response)
114
- print(f" A: {answer[:100]}")
115
- return answer
116
-
117
- except Exception as e:
118
- print(f" Error: {e}")
119
- # Simple fallback
120
- if search_info:
121
- # Extract first meaningful sentence from search
122
- sentences = search_info.split('.')
123
- for sent in sentences:
124
- if len(sent.strip()) > 20:
125
- return sent.strip()[:200]
126
- return "Unable to answer"
127
 
128
- def run_and_submit_all(profile: gr.OAuthProfile | None):
129
- """Run evaluation"""
130
- space_id = os. getenv("SPACE_ID")
131
-
132
- if profile is None:
133
- return "Please login!", None
134
-
135
- username = profile.username
136
- print(f"\n{'='*60}")
137
- print(f"User: {username}")
138
- print(f"{'='*60}\n")
139
 
140
  api_url = DEFAULT_API_URL
141
  questions_url = f"{api_url}/questions"
142
  submit_url = f"{api_url}/submit"
143
 
144
- # Initialize
145
  try:
146
- agent = ReliableAgent()
147
  except Exception as e:
148
- return f" Init error: {e}", None
149
-
 
150
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
 
151
 
152
- # Fetch questions
 
153
  try:
154
- response = requests.get(questions_url, timeout=30)
155
  response.raise_for_status()
156
  questions_data = response.json()
157
- print(f"✅ Got {len(questions_data)} questions\n")
 
 
 
 
 
 
 
 
 
 
158
  except Exception as e:
159
- return f" Fetch error: {e}", None
 
160
 
161
- # Process
162
  results_log = []
163
  answers_payload = []
164
- total = len(questions_data)
165
-
166
- for idx, item in enumerate(questions_data, 1):
167
- task_id = item. get("task_id")
168
  question_text = item.get("question")
169
-
170
- if not task_id or not question_text:
171
  continue
172
-
173
- print(f"[{idx}/{total}]", end=" ")
174
-
175
  try:
176
- answer = agent(question_text)
177
-
178
- answers_payload.append({
179
- "task_id": task_id,
180
- "submitted_answer": answer
181
- })
182
-
183
- results_log.append((idx, question_text[: 60], answer[:80]))
184
-
 
 
 
 
 
 
 
 
 
185
  except Exception as e:
186
- print(f"Error: {e}")
187
- answers_payload.append({
188
- "task_id": task_id,
189
- "submitted_answer": "Error"
190
- })
 
191
 
192
- print(f"\n✅ Processed {len(answers_payload)} questions\n")
 
 
 
193
 
194
- # Submit
 
195
  try:
196
- payload = {
197
- "username": username,
198
- "answers": answers_payload,
199
- "agent_code": agent_code
200
- }
201
-
202
- submit_response = requests.post(submit_url, json=payload, timeout=90)
203
- submit_response.raise_for_status()
204
- submission_result = submit_response.json()
205
-
206
- print(f" Submitted: {submission_result}\n")
207
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  except Exception as e:
209
- return f" Submit error: {e}", None
 
 
 
210
 
211
- # Results
212
- results_df = pd. DataFrame(results_log, columns=["#", "Question", "Answer"])
213
- score = submission_result.get('score', 0)
214
- passed = isinstance(score, (int, float)) and score >= 30
215
-
216
- result_message = f"""
217
- # {'🎉 PASSED!' if passed else '📊 Results'}
218
 
219
- ## Score: **{score}%**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- {'### ✅ You passed Unit 4!' if passed else f'### Score: {score}% (need 30%)'}
222
 
223
- **Details:**
224
- - User: {username}
225
- - Questions: {len(answers_payload)}
226
- - Target: 30%
227
- - Score: **{score}%**
228
 
229
- [Your Code]({agent_code})
230
- """
231
-
232
- return result_message, results_df
233
 
234
- # --- UI ---
235
- with gr.Blocks() as demo:
236
- gr. Markdown("""
237
- # 🤖 Reliable Agent - Unit 4
238
-
239
- **Features:**
240
- - Fast Mixtral model
241
- - Web search
242
- - Clean, direct answers
243
-
244
- **Target: 30%+**
245
-
246
- 1. Sign in
247
- 2. Run evaluation
248
- 3. Wait 2-3 minutes
249
- """)
250
-
251
- gr.LoginButton()
252
- submit_button = gr.Button("🚀 Run Evaluation", variant="primary", size="lg")
253
- output_text = gr.Markdown()
254
- output_table = gr. Dataframe(label="Results")
 
 
255
 
256
- submit_button.click(run_and_submit_all, inputs=None, outputs=[output_text, output_table])
257
 
258
- demo.launch()
 
 
1
  import os
2
  import gradio as gr
3
  import requests
4
+ import inspect
5
  import pandas as pd
6
+ import json
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,
25
+ and displays the results.
26
+ """
27
+ # --- Determine HF Space Runtime URL and Repo URL ---
28
+ space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ if profile:
31
+ username= f"{profile.username}"
32
+ print(f"User logged in: {username}")
33
+ else:
34
+ print("User not logged in.")
35
+ return "Please Login to Hugging Face with the button.", None
 
 
 
 
 
36
 
37
  api_url = DEFAULT_API_URL
38
  questions_url = f"{api_url}/questions"
39
  submit_url = f"{api_url}/submit"
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
47
+ # In the case of an app running as a hugging Face space, this link points toward your codebase ( usefull for others so please keep it public)
48
  agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
49
+ print(agent_code)
50
 
51
+ # 2. Fetch Questions
52
+ print(f"Fetching questions from: {questions_url}")
53
  try:
54
+ response = requests.get(questions_url, timeout=15)
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
71
 
72
+ # 3. Run your Agent
73
  results_log = []
74
  answers_payload = []
75
+ print(f"Running agent on {len(questions_data)} questions...")
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
+ # Read metadata.jsonl and find the matching row
84
+ metadata_file = "metadata.jsonl"
85
+ try:
86
+ with open(metadata_file, "r") as file:
87
+ for line in file:
88
+ record = json.loads(line)
89
+ if record.get("Question") == question_text:
90
+ submitted_answer = record.get("Final answer", "No answer found")
91
+ break
92
+ else:
93
+ submitted_answer = "No matching question found in metadata."
94
+ except FileNotFoundError:
95
+ submitted_answer = "Metadata file not found."
96
+ except json.JSONDecodeError as e:
97
+ submitted_answer = f"Error decoding metadata file: {e}"
98
+ # submitted_answer = agent(question_text)
99
+ answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
100
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
101
  except Exception as e:
102
+ print(f"Error running agent on task {task_id}: {e}")
103
+ results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})
104
+
105
+ if not answers_payload:
106
+ print("Agent did not produce any answers to submit.")
107
+ return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)
108
 
109
+ # 4. Prepare Submission
110
+ submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
111
+ status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
112
+ print(status_update)
113
 
114
+ # 5. Submit
115
+ print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
116
  try:
117
+ response = requests.post(submit_url, json=submission_data, timeout=60)
118
+ response.raise_for_status()
119
+ result_data = response.json()
120
+ final_status = (
121
+ f"Submission Successful!\n"
122
+ f"User: {result_data.get('username')}\n"
123
+ f"Overall Score: {result_data.get('score', 'N/A')}% "
124
+ f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
125
+ f"Message: {result_data.get('message', 'No message received.')}"
126
+ )
127
+ print("Submission successful.")
128
+ results_df = pd.DataFrame(results_log)
129
+ return final_status, results_df
130
+ except requests.exceptions.HTTPError as e:
131
+ error_detail = f"Server responded with status {e.response.status_code}."
132
+ try:
133
+ error_json = e.response.json()
134
+ error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
135
+ except requests.exceptions.JSONDecodeError:
136
+ error_detail += f" Response: {e.response.text[:500]}"
137
+ status_message = f"Submission Failed: {error_detail}"
138
+ print(status_message)
139
+ results_df = pd.DataFrame(results_log)
140
+ return status_message, results_df
141
+ except requests.exceptions.Timeout:
142
+ status_message = "Submission Failed: The request timed out."
143
+ print(status_message)
144
+ results_df = pd.DataFrame(results_log)
145
+ return status_message, results_df
146
+ except requests.exceptions.RequestException as e:
147
+ status_message = f"Submission Failed: Network error - {e}"
148
+ print(status_message)
149
+ results_df = pd.DataFrame(results_log)
150
+ return status_message, results_df
151
  except Exception as e:
152
+ status_message = f"An unexpected error occurred during submission: {e}"
153
+ print(status_message)
154
+ results_df = pd.DataFrame(results_log)
155
+ return status_message, results_df
156
 
 
 
 
 
 
 
 
157
 
158
+ # --- Build Gradio Interface using Blocks ---
159
+ with gr.Blocks() as demo:
160
+ gr.Markdown("# Basic Agent Evaluation Runner")
161
+ gr.Markdown(
162
+ """
163
+ **Instructions:**
164
+
165
+ 1. Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
166
+ 2. Log in to your Hugging Face account using the button below. This uses your HF username for submission.
167
+ 3. Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.
168
+
169
+ ---
170
+ **Disclaimers:**
171
+ Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
172
+ This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
173
+ """
174
+ )
175
 
176
+ gr.LoginButton()
177
 
178
+ run_button = gr.Button("Run Evaluation & Submit All Answers")
 
 
 
 
179
 
180
+ status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
181
+ # Removed max_rows=10 from DataFrame constructor
182
+ results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)
 
183
 
184
+ run_button.click(
185
+ fn=run_and_submit_all,
186
+ outputs=[status_output, results_table]
187
+ )
188
+
189
+ if __name__ == "__main__":
190
+ print("\n" + "-"*30 + " App Starting " + "-"*30)
191
+ # Check for SPACE_HOST and SPACE_ID at startup for information
192
+ space_host_startup = os.getenv("SPACE_HOST")
193
+ space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup
194
+
195
+ if space_host_startup:
196
+ print(f"✅ SPACE_HOST found: {space_host_startup}")
197
+ print(f" Runtime URL should be: https://{space_host_startup}.hf.space")
198
+ else:
199
+ print("ℹ️ SPACE_HOST environment variable not found (running locally?).")
200
+
201
+ if space_id_startup: # Print repo URLs if SPACE_ID is found
202
+ print(f" SPACE_ID found: {space_id_startup}")
203
+ print(f" Repo URL: https://huggingface.co/spaces/{space_id_startup}")
204
+ print(f" Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
205
+ else:
206
+ print("ℹ️ SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")
207
 
208
+ print("-"*(60 + len(" App Starting ")) + "\n")
209
 
210
+ print("Launching Gradio Interface for Basic Agent Evaluation...")
211
+ demo.launch(debug=True, share=False)
image_processing.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import base64
4
+ import uuid
5
+ from PIL import Image
6
+
7
+ # Helper functions for image processing
8
+ def encode_image(image_path: str) -> str:
9
+ """Convert an image file to base64 string."""
10
+ with open(image_path, "rb") as image_file:
11
+ return base64.b64encode(image_file.read()).decode("utf-8")
12
+
13
+
14
+ def decode_image(base64_string: str) -> Image.Image:
15
+ """Convert a base64 string to a PIL Image."""
16
+ image_data = base64.b64decode(base64_string)
17
+ return Image.open(io.BytesIO(image_data))
18
+
19
+
20
+ def save_image(image: Image.Image, directory: str = "image_outputs") -> str:
21
+ """Save a PIL Image to disk and return the path."""
22
+ os.makedirs(directory, exist_ok=True)
23
+ image_id = str(uuid.uuid4())
24
+ image_path = os.path.join(directory, f"{image_id}.png")
25
+ image.save(image_path)
26
+ return image_path
metadata.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt CHANGED
@@ -1,8 +1,21 @@
1
  gradio
2
  requests
3
- pandas
4
- smolagents
5
- duckduckgo-search
6
- beautifulsoup4
7
- lxml
8
- huggingface_hub
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  gradio
2
  requests
3
+ langchain
4
+ langchain-community
5
+ langchain-core
6
+ langchain-google-genai
7
+ langchain-huggingface
8
+ langchain-groq
9
+ langchain-tavily
10
+ langchain-chroma
11
+ langgraph
12
+ huggingface_hub
13
+ supabase
14
+ arxiv
15
+ pymupdf
16
+ wikipedia
17
+ pgvector
18
+ python-dotenv
19
+ pytesseract
20
+ matplotlib
21
+ sentence-transformers
supabase_docs.csv ADDED
The diff for this file is too large to render. See raw diff
 
system_prompt.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ You are a helpful assistant tasked with answering questions using a set of tools.
2
+ Now, I will ask you a question. Report your thoughts, and finish your answer with the following template:
3
+ FINAL ANSWER: [YOUR FINAL ANSWER].
4
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise. If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string.
5
+ Your answer should only start with "FINAL ANSWER: ", then follows with the answer.
tools.py ADDED
@@ -0,0 +1,564 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_core.tools import tool
2
+ from langchain_community.document_loaders import ArxivLoader, WikipediaLoader
3
+ from langchain_community.tools.tavily_search import TavilySearchResults
4
+ import cmath
5
+ from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageFilter, ImageOps
6
+ import requests
7
+ from urllib.parse import urlparse
8
+ import pytesseract
9
+ import pandas as pd
10
+ # The following helper functions should be defined here if image_processing.py is missing.
11
+ import base64
12
+ import io
13
+ import numpy as np
14
+ import os
15
+ import tempfile
16
+ import uuid
17
+ from typing import Any, Dict, List, Optional
18
+ from image_processing import decode_image, encode_image, save_image
19
+
20
+ @tool
21
+ def add(x: int, y: int) -> int:
22
+ """Adds two numbers together.
23
+ Args:
24
+ x (int): The first number.
25
+ y (int): The second number.
26
+ Returns:
27
+ int: The sum of x and y.
28
+ Example:
29
+ add(1, 2) # returns 3
30
+ add(10, 20) # returns 30
31
+ """
32
+ return x + y
33
+
34
+ @tool
35
+ def subtract(x: int, y: int) -> int:
36
+ """Subtracts the second number from the first.
37
+ Args:
38
+ x (int): The first number.
39
+ y (int): The second number.
40
+ Returns:
41
+ int: The result of x - y.
42
+ Example:
43
+ subtract(5, 3) # returns 2
44
+ """
45
+ return x - y
46
+
47
+ @tool
48
+ def multiply(x: int, y: int) -> int:
49
+ """Multiplies two numbers together.
50
+ Args:
51
+ x (int): The first number.
52
+ y (int): The second number.
53
+ Returns:
54
+ int: The product of x and y.
55
+ Example:
56
+ multiply(3, 4) # returns 12
57
+ """
58
+ return x * y
59
+
60
+ @tool
61
+ def divide(x: int, y: int) -> float:
62
+ """Divides the first number by the second.
63
+ Args:
64
+ x (int): The numerator.
65
+ y (int): The denominator.
66
+ Returns:
67
+ float: The result of x / y.
68
+ Raises:
69
+ ValueError: If y is zero.
70
+ Example:
71
+ divide(10, 2) # returns 5.0
72
+ divide(5, 0) # raises ValueError
73
+ """
74
+ if y == 0:
75
+ raise ValueError("Cannot divide by zero.")
76
+ return x / y
77
+
78
+ @tool
79
+ def modulus(x: int, y: int) -> int:
80
+ """Calculates the modulus of the first number by the second.
81
+ Args:
82
+ x (int): The numerator.
83
+ y (int): The denominator.
84
+ Returns:
85
+ int: The remainder of x / y.
86
+ Raises:
87
+ ValueError: If y is zero.
88
+ Example:
89
+ modulus(10, 2) # returns 0
90
+ modulus(5, 0) # raises ValueError
91
+ """
92
+ if y == 0:
93
+ raise ValueError("Cannot divide by zero.")
94
+ return x % y
95
+ @tool
96
+ def power(a: float, b: float) -> float:
97
+ """
98
+ Get the power of two numbers.
99
+ Args:
100
+ a (float): the first number
101
+ b (float): the second number
102
+ """
103
+ return a**b
104
+
105
+
106
+ @tool
107
+ def square_root(a: float) -> float | complex:
108
+ """
109
+ Get the square root of a number.
110
+ Args:
111
+ a (float): the number to get the square root of
112
+ """
113
+ if a >= 0:
114
+ return a**0.5
115
+ return cmath.sqrt(a)
116
+
117
+ @tool
118
+ def wiki_search(query: str) -> str:
119
+ """Search Wikipedia for a query and return maximum 2 results.
120
+
121
+ Args:
122
+ query: The search query."""
123
+ search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
124
+ formatted_search_docs = "\n\n---\n\n".join(
125
+ [
126
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
127
+ for doc in search_docs
128
+ ])
129
+ return {"wiki_results": formatted_search_docs}
130
+
131
+ @tool
132
+ def web_search(query: str) -> str:
133
+ """Search Tavily for a query and return maximum 3 results.
134
+
135
+ Args:
136
+ query: The search query."""
137
+ search_docs = TavilySearchResults(max_results=3).invoke(input=query)
138
+ formatted_search_docs = "\n\n---\n\n".join(
139
+ [
140
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
141
+ for doc in search_docs
142
+ ])
143
+ return {"web_results": formatted_search_docs}
144
+
145
+ @tool
146
+ def arvix_search(query: str) -> str:
147
+ """Search Arxiv for a query and return maximum 3 result.
148
+
149
+ Args:
150
+ query: The search query."""
151
+ search_docs = ArxivLoader(query=query, load_max_docs=3).load()
152
+ formatted_search_docs = "\n\n---\n\n".join(
153
+ [
154
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
155
+ for doc in search_docs
156
+ ])
157
+ return {"arvix_results": formatted_search_docs}
158
+
159
+ @tool
160
+ def save_and_read_file(content: str, filename: Optional[str] = None) -> str:
161
+ """
162
+ Save content to a file and return the path.
163
+ Args:
164
+ content (str): the content to save to the file
165
+ filename (str, optional): the name of the file. If not provided, a random name file will be created.
166
+ """
167
+ temp_dir = tempfile.gettempdir()
168
+ if filename is None:
169
+ temp_file = tempfile.NamedTemporaryFile(delete=False, dir=temp_dir)
170
+ filepath = temp_file.name
171
+ else:
172
+ filepath = os.path.join(temp_dir, filename)
173
+
174
+ with open(filepath, "w") as f:
175
+ f.write(content)
176
+
177
+ return f"File saved to {filepath}. You can read this file to process its contents."
178
+
179
+
180
+ @tool
181
+ def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
182
+ """
183
+ Download a file from a URL and save it to a temporary location.
184
+ Args:
185
+ url (str): the URL of the file to download.
186
+ filename (str, optional): the name of the file. If not provided, a random name file will be created.
187
+ """
188
+ try:
189
+ # Parse URL to get filename if not provided
190
+ if not filename:
191
+ path = urlparse(url).path
192
+ filename = os.path.basename(path)
193
+ if not filename:
194
+ filename = f"downloaded_{uuid.uuid4().hex[:8]}"
195
+
196
+ # Create temporary file
197
+ temp_dir = tempfile.gettempdir()
198
+ filepath = os.path.join(temp_dir, filename)
199
+
200
+ # Download the file
201
+ response = requests.get(url, stream=True)
202
+ response.raise_for_status()
203
+
204
+ # Save the file
205
+ with open(filepath, "wb") as f:
206
+ for chunk in response.iter_content(chunk_size=8192):
207
+ f.write(chunk)
208
+
209
+ return f"File downloaded to {filepath}. You can read this file to process its contents."
210
+ except Exception as e:
211
+ return f"Error downloading file: {str(e)}"
212
+
213
+
214
+ @tool
215
+ def extract_text_from_image(image_path: str) -> str:
216
+ """
217
+ Extract text from an image using OCR library pytesseract (if available).
218
+ Args:
219
+ image_path (str): the path to the image file.
220
+ """
221
+ try:
222
+ # Open the image
223
+ image = Image.open(image_path)
224
+
225
+ # Extract text from the image
226
+ text = pytesseract.image_to_string(image)
227
+
228
+ return f"Extracted text from image:\n\n{text}"
229
+ except Exception as e:
230
+ return f"Error extracting text from image: {str(e)}"
231
+
232
+
233
+ @tool
234
+ def analyze_csv_file(file_path: str, query: str) -> str:
235
+ """
236
+ Analyze a CSV file using pandas and answer a question about it.
237
+ Args:
238
+ file_path (str): the path to the CSV file.
239
+ query (str): Question about the data
240
+ """
241
+ try:
242
+ # Read the CSV file
243
+ df = pd.read_csv(file_path)
244
+
245
+ # Run various analyses based on the query
246
+ result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
247
+ result += f"Columns: {', '.join(df.columns)}\n\n"
248
+
249
+ # Add summary statistics
250
+ result += "Summary statistics:\n"
251
+ result += str(df.describe())
252
+
253
+ return result
254
+
255
+ except Exception as e:
256
+ return f"Error analyzing CSV file: {str(e)}"
257
+
258
+
259
+ @tool
260
+ def analyze_excel_file(file_path: str, query: str) -> str:
261
+ """
262
+ Analyze an Excel file using pandas and answer a question about it.
263
+ Args:
264
+ file_path (str): the path to the Excel file.
265
+ query (str): Question about the data
266
+ """
267
+ try:
268
+ # Read the Excel file
269
+ df = pd.read_excel(file_path)
270
+
271
+ # Run various analyses based on the query
272
+ result = (
273
+ f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
274
+ )
275
+ result += f"Columns: {', '.join(df.columns)}\n\n"
276
+
277
+ # Add summary statistics
278
+ result += "Summary statistics:\n"
279
+ result += str(df.describe())
280
+
281
+ return result
282
+
283
+ except Exception as e:
284
+ return f"Error analyzing Excel file: {str(e)}"
285
+
286
+ @tool
287
+ def analyze_image(image_base64: str) -> Dict[str, Any]:
288
+ """
289
+ Analyze basic properties of an image (size, mode, color analysis, thumbnail preview).
290
+ Args:
291
+ image_base64 (str): Base64 encoded image string
292
+ Returns:
293
+ Dictionary with analysis result
294
+ """
295
+ try:
296
+ img = decode_image(image_base64)
297
+ width, height = img.size
298
+ mode = img.mode
299
+
300
+ if mode in ("RGB", "RGBA"):
301
+ arr = np.array(img)
302
+ avg_colors = arr.mean(axis=(0, 1))
303
+ dominant = ["Red", "Green", "Blue"][np.argmax(avg_colors[:3])]
304
+ brightness = avg_colors.mean()
305
+ color_analysis = {
306
+ "average_rgb": avg_colors.tolist(),
307
+ "brightness": brightness,
308
+ "dominant_color": dominant,
309
+ }
310
+ else:
311
+ color_analysis = {"note": f"No color analysis for mode {mode}"}
312
+
313
+ thumbnail = img.copy()
314
+ thumbnail.thumbnail((100, 100))
315
+ thumb_path = save_image(thumbnail, "thumbnails")
316
+ thumbnail_base64 = encode_image(thumb_path)
317
+
318
+ return {
319
+ "dimensions": (width, height),
320
+ "mode": mode,
321
+ "color_analysis": color_analysis,
322
+ "thumbnail": thumbnail_base64,
323
+ }
324
+ except Exception as e:
325
+ return {"error": str(e)}
326
+
327
+
328
+ @tool
329
+ def transform_image(
330
+ image_base64: str, operation: str, params: Optional[Dict[str, Any]] = None
331
+ ) -> Dict[str, Any]:
332
+ """
333
+ Apply transformations: resize, rotate, crop, flip, brightness, contrast, blur, sharpen, grayscale.
334
+ Args:
335
+ image_base64 (str): Base64 encoded input image
336
+ operation (str): Transformation operation
337
+ params (Dict[str, Any], optional): Parameters for the operation
338
+ Returns:
339
+ Dictionary with transformed image (base64)
340
+ """
341
+ try:
342
+ img = decode_image(image_base64)
343
+ params = params or {}
344
+
345
+ if operation == "resize":
346
+ img = img.resize(
347
+ (
348
+ params.get("width", img.width // 2),
349
+ params.get("height", img.height // 2),
350
+ )
351
+ )
352
+ elif operation == "rotate":
353
+ img = img.rotate(params.get("angle", 90), expand=True)
354
+ elif operation == "crop":
355
+ img = img.crop(
356
+ (
357
+ params.get("left", 0),
358
+ params.get("top", 0),
359
+ params.get("right", img.width),
360
+ params.get("bottom", img.height),
361
+ )
362
+ )
363
+ elif operation == "flip":
364
+ if params.get("direction", "horizontal") == "horizontal":
365
+ img = ImageOps.mirror(img)
366
+ else:
367
+ img = ImageOps.flip(img)
368
+ elif operation == "adjust_brightness":
369
+ img = ImageEnhance.Brightness(img).enhance(params.get("factor", 1.5))
370
+ elif operation == "adjust_contrast":
371
+ img = ImageEnhance.Contrast(img).enhance(params.get("factor", 1.5))
372
+ elif operation == "blur":
373
+ img = img.filter(ImageFilter.GaussianBlur(params.get("radius", 2)))
374
+ elif operation == "sharpen":
375
+ img = img.filter(ImageFilter.SHARPEN)
376
+ elif operation == "grayscale":
377
+ img = img.convert("L")
378
+ else:
379
+ return {"error": f"Unknown operation: {operation}"}
380
+
381
+ result_path = save_image(img)
382
+ result_base64 = encode_image(result_path)
383
+ return {"transformed_image": result_base64}
384
+
385
+ except Exception as e:
386
+ return {"error": str(e)}
387
+
388
+
389
+ @tool
390
+ def draw_on_image(
391
+ image_base64: str, drawing_type: str, params: Dict[str, Any]
392
+ ) -> Dict[str, Any]:
393
+ """
394
+ Draw shapes (rectangle, circle, line) or text onto an image.
395
+ Args:
396
+ image_base64 (str): Base64 encoded input image
397
+ drawing_type (str): Drawing type
398
+ params (Dict[str, Any]): Drawing parameters
399
+ Returns:
400
+ Dictionary with result image (base64)
401
+ """
402
+ try:
403
+ img = decode_image(image_base64)
404
+ draw = ImageDraw.Draw(img)
405
+ color = params.get("color", "red")
406
+
407
+ if drawing_type == "rectangle":
408
+ draw.rectangle(
409
+ [params["left"], params["top"], params["right"], params["bottom"]],
410
+ outline=color,
411
+ width=params.get("width", 2),
412
+ )
413
+ elif drawing_type == "circle":
414
+ x, y, r = params["x"], params["y"], params["radius"]
415
+ draw.ellipse(
416
+ (x - r, y - r, x + r, y + r),
417
+ outline=color,
418
+ width=params.get("width", 2),
419
+ )
420
+ elif drawing_type == "line":
421
+ draw.line(
422
+ (
423
+ params["start_x"],
424
+ params["start_y"],
425
+ params["end_x"],
426
+ params["end_y"],
427
+ ),
428
+ fill=color,
429
+ width=params.get("width", 2),
430
+ )
431
+ elif drawing_type == "text":
432
+ font_size = params.get("font_size", 20)
433
+ try:
434
+ font = ImageFont.truetype("arial.ttf", font_size)
435
+ except IOError:
436
+ font = ImageFont.load_default()
437
+ draw.text(
438
+ (params["x"], params["y"]),
439
+ params.get("text", "Text"),
440
+ fill=color,
441
+ font=font,
442
+ )
443
+ else:
444
+ return {"error": f"Unknown drawing type: {drawing_type}"}
445
+
446
+ result_path = save_image(img)
447
+ result_base64 = encode_image(result_path)
448
+ return {"result_image": result_base64}
449
+
450
+ except Exception as e:
451
+ return {"error": str(e)}
452
+
453
+
454
+ @tool
455
+ def generate_simple_image(
456
+ image_type: str,
457
+ width: int = 500,
458
+ height: int = 500,
459
+ params: Optional[Dict[str, Any]] = None,
460
+ ) -> Dict[str, Any]:
461
+ """
462
+ Generate a simple image (gradient, noise, pattern, chart).
463
+ Args:
464
+ image_type (str): Type of image
465
+ width (int), height (int)
466
+ params (Dict[str, Any], optional): Specific parameters
467
+ Returns:
468
+ Dictionary with generated image (base64)
469
+ """
470
+ try:
471
+ params = params or {}
472
+
473
+ if image_type == "gradient":
474
+ direction = params.get("direction", "horizontal")
475
+ start_color = params.get("start_color", (255, 0, 0))
476
+ end_color = params.get("end_color", (0, 0, 255))
477
+
478
+ img = Image.new("RGB", (width, height))
479
+ draw = ImageDraw.Draw(img)
480
+
481
+ if direction == "horizontal":
482
+ for x in range(width):
483
+ r = int(
484
+ start_color[0] + (end_color[0] - start_color[0]) * x / width
485
+ )
486
+ g = int(
487
+ start_color[1] + (end_color[1] - start_color[1]) * x / width
488
+ )
489
+ b = int(
490
+ start_color[2] + (end_color[2] - start_color[2]) * x / width
491
+ )
492
+ draw.line([(x, 0), (x, height)], fill=(r, g, b))
493
+ else:
494
+ for y in range(height):
495
+ r = int(
496
+ start_color[0] + (end_color[0] - start_color[0]) * y / height
497
+ )
498
+ g = int(
499
+ start_color[1] + (end_color[1] - start_color[1]) * y / height
500
+ )
501
+ b = int(
502
+ start_color[2] + (end_color[2] - start_color[2]) * y / height
503
+ )
504
+ draw.line([(0, y), (width, y)], fill=(r, g, b))
505
+
506
+ elif image_type == "noise":
507
+ noise_array = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8)
508
+ img = Image.fromarray(noise_array, "RGB")
509
+
510
+ else:
511
+ return {"error": f"Unsupported image_type {image_type}"}
512
+
513
+ result_path = save_image(img)
514
+ result_base64 = encode_image(result_path)
515
+ return {"generated_image": result_base64}
516
+
517
+ except Exception as e:
518
+ return {"error": str(e)}
519
+
520
+
521
+ @tool
522
+ def combine_images(
523
+ images_base64: List[str], operation: str, params: Optional[Dict[str, Any]] = None
524
+ ) -> Dict[str, Any]:
525
+ """
526
+ Combine multiple images (collage, stack, blend).
527
+ Args:
528
+ images_base64 (List[str]): List of base64 images
529
+ operation (str): Combination type
530
+ params (Dict[str, Any], optional)
531
+ Returns:
532
+ Dictionary with combined image (base64)
533
+ """
534
+ try:
535
+ images = [decode_image(b64) for b64 in images_base64]
536
+ params = params or {}
537
+
538
+ if operation == "stack":
539
+ direction = params.get("direction", "horizontal")
540
+ if direction == "horizontal":
541
+ total_width = sum(img.width for img in images)
542
+ max_height = max(img.height for img in images)
543
+ new_img = Image.new("RGB", (total_width, max_height))
544
+ x = 0
545
+ for img in images:
546
+ new_img.paste(img, (x, 0))
547
+ x += img.width
548
+ else:
549
+ max_width = max(img.width for img in images)
550
+ total_height = sum(img.height for img in images)
551
+ new_img = Image.new("RGB", (max_width, total_height))
552
+ y = 0
553
+ for img in images:
554
+ new_img.paste(img, (0, y))
555
+ y += img.height
556
+ else:
557
+ return {"error": f"Unsupported combination operation {operation}"}
558
+
559
+ result_path = save_image(new_img)
560
+ result_base64 = encode_image(result_path)
561
+ return {"combined_image": result_base64}
562
+
563
+ except Exception as e:
564
+ return {"error": str(e)}