File size: 6,712 Bytes
571ff03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# web_agent.py
# Minimal web-searching LangChain agent for GAIA-style questions.
# - Tools: DuckDuckGo search + HTTP GET
# - Model: defaults to a lightweight reasoning-ready open model (Hugging Face), but you can
#          swap to any chat model supported by LangChain. If you don't set HF token, we'll
#          fall back to a simple reasoning prompt with the same interface.

from __future__ import annotations

import argparse
import os
from typing import List, Optional, Dict, Any

# LangChain core (v0.2+ / v0.3)
from langchain.agents import AgentExecutor, create_react_agent
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnableConfig
from langchain_community.utilities import RequestsWrapper
from langchain.tools import Tool

# Chat model: Hugging Face Endpoint (no key required to run, but highly recommended)
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint

# Community tools
from langchain_community.tools import DuckDuckGoSearchRun, RequestsGetTool

# ---- Prompt (self-contained: no hub dependency) ----
REACT_PROMPT = """You are a meticulous research assistant for GAIA-style fact questions.



You have access to these tools:

{tools}



Use this format:



Question: the input question to answer

Thought: think about what to search or read next

Action: one of [{tool_names}]

Action Input: the input to the action

Observation: the result of the action

... (repeat Thought/Action/Observation as needed)

Thought: I can now answer.

Final Answer: <the shortest exact answer>



Follow the rules:

- Prefer precise quotes from primary/official sources when possible.

- Keep the final answer SHORT (a few words if possible)

- If you need multiple pages, open them sequentially.

- Never repeat the exact same Action Input twice. If an Action returns a login, auth, or error page, change strategy (use search with site: filters, try a different public page, or a cached/mirrored source).

- If a URL contains "login", "@app/auth", or "returnto=", do NOT open it; search for a public page instead.



Question: {input}

{agent_scratchpad}"""

# Final Answer: <the shortest exact answer> — <single best source URL>
# - Keep the final answer SHORT (a few words if possible) plus one URL.
class WebAgent:
    """Encapsulated GAIA-style web agent.



    Usage:

        web_agent = WebAgent()

        result = web_agent("Who is the CEO of ...?")

        # or

        result = web_agent.answer("...")



    You can also run as a script:

        python web_agent.py "Your question here"

    """

    def __init__(

        self,

        hf_repo_id: Optional[str] = None,

        max_new_tokens: int = 2048,

        do_sample: bool = False,

        repetition_penalty: float = 1.05,

        max_iterations: int = 20,

        verbose: bool = True,

        user_agent: str = "Mozilla/5.0 (compatible; GAIA/1.0; +https://example.org/bot)",

    ):
        self.hf_repo_id = hf_repo_id or os.environ.get("HF_REPO_ID", "Qwen/Qwen2.5-7B-Instruct")
        self.max_new_tokens = max_new_tokens
        self.do_sample = do_sample
        self.repetition_penalty = repetition_penalty
        self.max_iterations = max_iterations
        self.verbose = verbose
        self.user_agent = user_agent

        # Built lazily so each call gets a fresh per-run "seen URL" set for the guarded GET.
        self._executor: Optional[AgentExecutor] = None
        self._tools: Optional[List[Tool]] = None

    # ---- Public API ----
    def answer(self, question: str) -> str:
        """Run the agent and return the model's 'Final Answer' string."""
        executor = self._build_agent()  # build fresh each time to reset guarded state
        result: Dict[str, Any] = executor.invoke({"input": question})
        return result["output"]

    def __call__(self, question: str) -> str:
        return self.answer(question)

    # ---- Internals ----
    def _build_model(self) -> ChatHuggingFace:
        llm = HuggingFaceEndpoint(
            repo_id=self.hf_repo_id,
            task="text-generation",
            max_new_tokens=self.max_new_tokens,
            do_sample=self.do_sample,
            repetition_penalty=self.repetition_penalty,
        )
        return ChatHuggingFace(llm=llm)

    def _build_tools(self) -> List[Tool]:
        wrapper = RequestsWrapper(headers={"User-Agent": self.user_agent})
        raw_get = RequestsGetTool(
            name="Requests Get (raw)",
            requests_wrapper=wrapper,
            allow_dangerous_requests=True,
        )

        seen: set[str] = set()

        def guarded_get(url: str) -> str:
            low = url.lower()
            if any(x in low for x in ["@app/auth", "login?returnto=", "/login"]):
                return ("BLOCKED: This looks like a login/auth wall. "
                        "Use search to find a public or cached page instead.")
            if url in seen:
                return (f"BLOCKED: Already fetched {url}. "
                        "Choose a different URL or try a different query.")
            seen.add(url)
            return raw_get.run(url)

        return [
            DuckDuckGoSearchRun(name="DuckDuckGo Search"),
            Tool.from_function(
                name="Requests Get",
                func=guarded_get,
                description="HTTP GET for public pages; prevents repeats and login walls.",
            ),
        ]

    def _build_agent(self) -> AgentExecutor:
        model = self._build_model()
        tools = self._build_tools()
        prompt = PromptTemplate.from_template(REACT_PROMPT)

        agent = create_react_agent(model, tools, prompt)
        executor = AgentExecutor(
            agent=agent,
            tools=tools,
            max_iterations=self.max_iterations,
            handle_parsing_errors=True,
            verbose=self.verbose,
        )
        return executor


# def main():
#     parser = argparse.ArgumentParser(description="Simple LangChain web agent for GAIA-style questions.")
#     parser.add_argument("question", type=str, help="Your question (quoted).")
#     args = parser.parse_args()

#     print(answer(args.question))


# if __name__ == "__main__":
#     main()

# ---- CLI Entrypoint ----
def main():
    parser = argparse.ArgumentParser(description="Class-based LangChain web agent for GAIA-style questions.")
    parser.add_argument("question", type=str, help="Your question (quoted).")
    args = parser.parse_args()

    web_agent = WebAgent()
    print(web_agent.answer(args.question))


if __name__ == "__main__":
    main()