ShoaibSSM commited on
Commit
7274fed
·
verified ·
1 Parent(s): 331099c

Upload 11 files

Browse files
Files changed (11) hide show
  1. .env.example +3 -0
  2. .gitignore +12 -0
  3. .python-version +1 -0
  4. Dockerfile +33 -0
  5. LICENSE +21 -0
  6. __init__.py +0 -0
  7. agent.py +185 -0
  8. main.py +61 -0
  9. pyproject.toml +38 -0
  10. shared_store.py +2 -0
  11. uv.lock +0 -0
.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ GOOGLE_API_KEY=your_gemini_api_key
2
+ EMAIL=your_email
3
+ SECRET=your_secret
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ .env
9
+ # Virtual environments
10
+ .venv
11
+ tests
12
+ LLMFiles
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.12
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # --- System deps required by Playwright browsers ---
4
+ RUN apt-get update && apt-get install -y \
5
+ wget gnupg ca-certificates curl unzip \
6
+ libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 \
7
+ libgtk-3-0 libgbm1 libasound2 libxcomposite1 libxdamage1 libxrandr2 \
8
+ libxfixes3 libpango-1.0-0 libcairo2 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # --- Install Playwright + Chromium ---
12
+ RUN pip install playwright && playwright install --with-deps chromium
13
+
14
+ # --- Install uv package manager ---
15
+ RUN pip install uv
16
+
17
+ # --- Copy app to container ---
18
+ WORKDIR /app
19
+
20
+ COPY . .
21
+
22
+ ENV PYTHONUNBUFFERED=1
23
+ ENV PYTHONIOENCODING=utf-8
24
+
25
+ # --- Install project dependencies using uv ---
26
+ RUN uv sync --frozen
27
+
28
+ # HuggingFace Spaces exposes port 7860
29
+ EXPOSE 7860
30
+
31
+ # --- Run your FastAPI app ---
32
+ # uvicorn must be in pyproject dependencies
33
+ CMD ["uv", "run", "main.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sai Vijay Ragav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
__init__.py ADDED
File without changes
agent.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, END, START
2
+ from shared_store import url_time
3
+ import time
4
+ from langchain_core.rate_limiters import InMemoryRateLimiter
5
+ from langgraph.prebuilt import ToolNode
6
+ from tools import (
7
+ get_rendered_html, download_file, post_request,
8
+ run_code, add_dependencies, ocr_image_tool, transcribe_audio, encode_image_to_base64
9
+ )
10
+ from typing import TypedDict, Annotated, List
11
+ from langchain_core.messages import trim_messages
12
+ from langchain.chat_models import init_chat_model
13
+ from langgraph.graph.message import add_messages
14
+ import os
15
+ from dotenv import load_dotenv
16
+ load_dotenv()
17
+
18
+ EMAIL = os.getenv("EMAIL")
19
+ SECRET = os.getenv("SECRET")
20
+
21
+ RECURSION_LIMIT = 5000
22
+ MAX_TOKENS = 180000
23
+
24
+
25
+ # -------------------------------------------------
26
+ # STATE
27
+ # -------------------------------------------------
28
+ class AgentState(TypedDict):
29
+ messages: Annotated[List, add_messages]
30
+
31
+
32
+ TOOLS = [
33
+ run_code, get_rendered_html, download_file,
34
+ post_request, add_dependencies, ocr_image_tool, transcribe_audio, encode_image_to_base64
35
+ ]
36
+
37
+
38
+ # -------------------------------------------------
39
+ # LLM INIT (NO SYSTEM PROMPT HERE)
40
+ # -------------------------------------------------
41
+ rate_limiter = InMemoryRateLimiter(
42
+ requests_per_second=7 / 60,
43
+ check_every_n_seconds=1,
44
+ max_bucket_size=7
45
+ )
46
+
47
+ llm = init_chat_model(
48
+ model_provider="google_genai",
49
+ model="gemini-2.5-flash",
50
+ rate_limiter=rate_limiter
51
+ ).bind_tools(TOOLS)
52
+
53
+
54
+
55
+ # -------------------------------------------------
56
+ # SYSTEM PROMPT (WILL BE INSERTED ONLY ONCE)
57
+ # -------------------------------------------------
58
+ SYSTEM_PROMPT = f"""
59
+ You are an autonomous quiz-solving agent.
60
+
61
+ Your job is to:
62
+ 1. Load each quiz page from the given URL.
63
+ 2. Extract instructions, parameters, and submit endpoint.
64
+ 3. Solve tasks exactly.
65
+ 4. Submit answers ONLY to the correct endpoint.
66
+ 5. Follow new URLs until none remain, then output END.
67
+
68
+ Rules:
69
+ - For base64 generation of an image NEVER use your own code, always use the "encode_image_to_base64" tool that's provided
70
+ - Never hallucinate URLs or fields.
71
+ - Never shorten endpoints.
72
+ - Always inspect server response.
73
+ - Never stop early.
74
+ - Use tools for HTML, downloading, rendering, OCR, or running code.
75
+ - Include:
76
+ email = {EMAIL}
77
+ secret = {SECRET}
78
+ """
79
+
80
+
81
+ # -------------------------------------------------
82
+ # AGENT NODE
83
+ # -------------------------------------------------
84
+ def agent_node(state: AgentState):
85
+ # time-handling
86
+ cur_time = time.time()
87
+ cur_url = os.getenv("url")
88
+ prev_time = url_time[cur_url]
89
+ offset = os.getenv("offset")
90
+ if prev_time is not None:
91
+ prev_time = float(prev_time)
92
+ diff = cur_time - prev_time
93
+
94
+ if diff >= 180 or (offset != "0" and (cur_time - float(offset)) > 90):
95
+ print("Timeout exceeded — instructing LLM to purposely submit wrong answer.", diff, "Offset=", offset)
96
+
97
+ fail_instruction = """
98
+ You have exceeded the time limit for this task (over 130 seconds).
99
+ Immediately call the `post_request` tool and submit a WRONG answer for the CURRENT quiz.
100
+ """
101
+
102
+ # LLM will figure out the right endpoint + JSON structure itself
103
+ result = llm.invoke([
104
+ {"role": "user", "content": fail_instruction}
105
+ ])
106
+ return {"messages": [result]}
107
+
108
+ trimmed_messages = trim_messages(
109
+ messages=state["messages"],
110
+ max_tokens=MAX_TOKENS,
111
+ strategy="last",
112
+ include_system=True,
113
+ start_on="human",
114
+ token_counter=llm, # Use the LLM to count actual tokens, not just list length
115
+ )
116
+
117
+ result = llm.invoke(trimmed_messages)
118
+
119
+ return {"messages": [result]}
120
+
121
+ # -------------------------------------------------
122
+ # ROUTE LOGIC (YOURS WITH MINOR SAFETY IMPROVES)
123
+ # -------------------------------------------------
124
+ def route(state):
125
+ last = state["messages"][-1]
126
+ # print("=== ROUTE DEBUG: last message type ===")
127
+
128
+ tool_calls = getattr(last, "tool_calls", None)
129
+
130
+ if tool_calls:
131
+ print("Route → tools")
132
+ return "tools"
133
+
134
+ content = getattr(last, "content", None)
135
+
136
+ if isinstance(content, str) and content.strip() == "END":
137
+ return END
138
+
139
+ if isinstance(content, list) and len(content) and isinstance(content[0], dict):
140
+ if content[0].get("text", "").strip() == "END":
141
+ return END
142
+
143
+ print("Route → agent")
144
+ return "agent"
145
+
146
+
147
+
148
+ # -------------------------------------------------
149
+ # GRAPH
150
+ # -------------------------------------------------
151
+ graph = StateGraph(AgentState)
152
+
153
+ graph.add_node("tools", ToolNode(TOOLS))
154
+
155
+ graph.add_edge(START, "agent")
156
+ graph.add_edge("tools", "agent")
157
+ graph.add_conditional_edges("agent", route)
158
+ robust_retry = {
159
+ "initial_interval": 1,
160
+ "backoff_factor": 2,
161
+ "max_interval": 60,
162
+ "max_attempts": 10
163
+ }
164
+
165
+ graph.add_node("agent", agent_node, retry=robust_retry)
166
+ app = graph.compile()
167
+
168
+
169
+
170
+ # -------------------------------------------------
171
+ # RUNNER
172
+ # -------------------------------------------------
173
+ def run_agent(url: str):
174
+ # system message is seeded ONCE here
175
+ initial_messages = [
176
+ {"role": "system", "content": SYSTEM_PROMPT},
177
+ {"role": "user", "content": url}
178
+ ]
179
+
180
+ app.invoke(
181
+ {"messages": initial_messages},
182
+ config={"recursion_limit": RECURSION_LIMIT}
183
+ )
184
+
185
+ print("Tasks completed successfully!")
main.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, BackgroundTasks
2
+ from fastapi.responses import JSONResponse
3
+ from fastapi.exceptions import HTTPException
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from agent import run_agent
6
+ from dotenv import load_dotenv
7
+ import uvicorn
8
+ import os
9
+ from shared_store import url_time, BASE64_STORE
10
+ import time
11
+
12
+ load_dotenv()
13
+
14
+ EMAIL = os.getenv("EMAIL")
15
+ SECRET = os.getenv("SECRET")
16
+
17
+ app = FastAPI()
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"], # or specific domains
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+ START_TIME = time.time()
26
+ @app.get("/healthz")
27
+ def healthz():
28
+ """Simple liveness check."""
29
+ return {
30
+ "status": "ok",
31
+ "uptime_seconds": int(time.time() - START_TIME)
32
+ }
33
+
34
+ @app.post("/solve")
35
+ async def solve(request: Request, background_tasks: BackgroundTasks):
36
+ try:
37
+ data = await request.json()
38
+ except Exception:
39
+ raise HTTPException(status_code=400, detail="Invalid JSON")
40
+ if not data:
41
+ raise HTTPException(status_code=400, detail="Invalid JSON")
42
+ url = data.get("url")
43
+ secret = data.get("secret")
44
+ if not url or not secret:
45
+ raise HTTPException(status_code=400, detail="Invalid JSON")
46
+
47
+ if secret != SECRET:
48
+ raise HTTPException(status_code=403, detail="Invalid secret")
49
+ url_time.clear()
50
+ BASE64_STORE.clear()
51
+ print("Verified starting the task...")
52
+ os.environ["url"] = url
53
+ os.environ["offset"] = "0"
54
+ url_time[url] = time.time()
55
+ background_tasks.add_task(run_agent, url)
56
+
57
+ return JSONResponse(status_code=200, content={"status": "ok"})
58
+
59
+
60
+ if __name__ == "__main__":
61
+ uvicorn.run(app, host="0.0.0.0", port=7860)
pyproject.toml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "tdsproject2"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "playwright>=1.56.0",
9
+ "beautifulsoup4>=4.14.2",
10
+ "langgraph>=1.0.3",
11
+ "langchain>=0.2.0",
12
+ "langchain-community>=0.2.0",
13
+ "langchain-google-genai>=1.0.0",
14
+ "google-genai>=0.17.0",
15
+ "jsonpatch>=1.33",
16
+ "python-dotenv>=1.2.1",
17
+ "pandas>=2.3.3",
18
+ "fastapi>=0.121.3",
19
+ "uvicorn>=0.38.0",
20
+ "requests>=2.32.5",
21
+ "pillow>=12.0.0",
22
+ "pytesseract>=0.3.13",
23
+ "speechrecognition>=3.14.4",
24
+ "pydub>=0.25.1",
25
+ "geopy>=2.4.1",
26
+ "scikit-learn>=1.7.2",
27
+ "matplotlib>=3.10.7",
28
+ "pypdf2>=3.0.1",
29
+ "ffmpeg-python>=0.2.0",
30
+ "numpy>=2.3.5",
31
+ "networkx>=3.6",
32
+ "fuzzywuzzy>=0.18.0",
33
+ "python-levenshtein>=0.27.3",
34
+ "duckdb>=1.4.2",
35
+ "pypdf>=6.4.0",
36
+ "scipy>=1.16.3",
37
+ "haversine>=2.9.0",
38
+ ]
shared_store.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ BASE64_STORE = {}
2
+ url_time = {}
uv.lock ADDED
The diff for this file is too large to render. See raw diff