pabloescobar18/Final_Assignment_Template
0
1"""LangGraph Agent"""2 3import os4import requests5from dotenv import load_dotenv6from bs4 import BeautifulSoup7from langchain_community.document_loaders import WikipediaLoader8from langchain_community.document_loaders import ArxivLoader9from langchain_community.utilities import WikipediaAPIWrapper10from langchain_core.messages import HumanMessage, SystemMessage11from langchain_core.tools import tool12from langchain_google_genai import ChatGoogleGenerativeAI13from langchain_community.tools.tavily_search import TavilySearchResults14from langchain_tavily import TavilySearch15from langchain_groq import ChatGroq16from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint17from langgraph.graph import START, MessagesState, StateGraph18from langgraph.prebuilt import ToolNode, tools_condition19from tools.web_search import duckduckgo_web_search20from tools.code_interpreter import python_interpreter21from tools.transcribe_audio import transcribe_audio22import wikipedia23 24 25 26load_dotenv()27 28groq_api_key = os.getenv("GROQ_API_KEY")29print(30 f"DEBUG: GROQ_API_KEY encontrada: {bool(os.getenv('GROQ_API_KEY'))}"31)32 33# load the system prompt from the file34with open("system_prompt.txt", "r", encoding="utf-8") as f:35 system_prompt = f.read()# System message36sys_msg = SystemMessage(content=system_prompt)37 38 39 40wikipedia.set_user_agent(41 "GAIA_LangGraph_Agent/1.0"42 " (https://huggingface.co/agents-course; student_agent@example.com)"43)44# Instancia el wrapper fuera de la función para no re-crearlo en cada llamada45api_wrapper = WikipediaAPIWrapper(46 top_k_results=2, # Máximo 2 resultados47 doc_content_chars_max=3500, # Limita el tamaño de cada texto48 lang="en" # Idioma (cambia a "en" si buscas en inglés)49)50 51@tool52def wikipedia_search(query: str) -> str:53 """Search English Wikipedia for a query and return rich document context with source URLs.54 55 Args:56 query: The search query string or topic name.57 """58 try:59 # .load() devuelve una lista de objetos Document60 docs = api_wrapper.load(query)61 62 if not docs:63 return f"No results found on Wikipedia for '{query}'."64 65 formatted_docs = []66 for doc in docs:67 # Extraer los metadatos que extrae WikipediaAPIWrapper68 source_url = doc.metadata.get(69 "source", f"https://en.wikipedia.org/wiki/{query}"70 )71 title = doc.metadata.get("title", query)72 73 # Formato XML enriquecido para que el LLM tenga contexto y la URL directa74 formatted_doc = (75 f'<Document title="{title}" source="{source_url}">\n'76 f"{doc.page_content}\n"77 f"</Document>"78 )79 formatted_docs.append(formatted_doc)80 81 return "\n\n---\n\n".join(formatted_docs)82 83 except Exception as e:84 return (85 f"Error searching Wikipedia: {str(e)}. Fallback to using"86 " 'duckduckgo_web_search'."87 )88 89@tool90def fetch_web_content(url: str) -> str:91 """Downloads and extracts clean plain text from a web page or file URL.92 93 Use this tool when you need to read the full content of a specific web URL,94 plain text file, or source code file. It automatically strips HTML tags,95 scripts, and extra whitespace to return readable text.96 97 Args:98 url: The full HTTP or HTTPS URL of the webpage or document to fetch (e.g.,99 'https://example.com/file.txt').100 101 Returns:102 str: The clean text content of the page (truncated if too long), or an103 error message if the request fails.104 """105 try:106 headers = {"User-Agent": "Mozilla/5.0"}107 response = requests.get(url, headers=headers, timeout=10)108 response.raise_for_status()109 110 # Convertir el HTML a texto plano usando BeautifulSoup111 soup = BeautifulSoup(response.text, "html.parser")112 113 # Eliminar scripts y estilos CSS114 for script in soup(["script", "style"]):115 script.decompose()116 117 text = soup.get_text(separator="\n")118 119 # Limpiar espacios en blanco excesivos120 lines = (line.strip() for line in text.splitlines())121 chunks = (phrase.strip() for line in lines for phrase in line.split(" "))122 clean_text = "\n".join(chunk for chunk in chunks if chunk)123 124 # RECORTE DE SEGURIDAD: Máximo 4000 caracteres para no saturar al LLM125 if len(clean_text) > 4000:126 clean_text = (127 clean_text[:4000]128 + "\n... [Texto truncado por longitud excesiva] ..."129 )130 131 return clean_text132 133 except Exception as e:134 return f"Error al obtener el contenido de la URL: {e}"135 136 137@tool138def multiply(a: int, b: int) -> int:139 """Multiply two numbers."""140 return a * b141 142 143@tool144def add(a: int, b: int) -> int:145 """Add two numbers."""146 return a + b147 148 149@tool150def subtract(a: int, b: int) -> int:151 """Subtract two numbers."""152 return a - b153 154@tool155def web_search(query: str) -> str:156 """Search Tavily for a query and return maximum 3 results.157 158 Args:159 query: The search query."""160 search_docs = TavilySearch(max_results=3).invoke({"query": query})161 162 formatted_search_docs = "\n\n---\n\n".join(163 [164 f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'165 for doc in search_docs166 ])167 return {"web_results": formatted_search_docs}168 169# Instanciar la tool nativa con la configuración deseada170web_search = TavilySearch(max_results=3)171 172 173@tool174def arvix_search(query: str) -> str:175 """Search Arxiv for a query and return maximum 3 result.176 177 Args:178 query: The search query."""179 search_docs = ArxivLoader(query=query, load_max_docs=3).load()180 formatted_search_docs = "\n\n---\n\n".join(181 [182 f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'183 for doc in search_docs184 ])185 return {"arvix_results": formatted_search_docs}186 187 188tools = [multiply, add, subtract, wikipedia_search,189 arvix_search, 190 web_search, python_interpreter,191 transcribe_audio]192#fetch_web_content193 194def build_graph(provider: str = "groq"):195 """Build the graph"""196 197 if provider == "huggingface":198 # Lee el token guardado en los Secrets de tu Space199 hf_token = os.getenv("HUGGINGFACEHUB_API_TOKEN") or os.getenv("HF_TOKEN")200 201 endpoint = HuggingFaceEndpoint(202 repo_id="Qwen/Qwen2.5-Coder-32B-Instruct", # Excelente para tareas de agentes y herramientas203 task="text-generation",204 temperature=0.1)205 206 llm = ChatHuggingFace(llm=endpoint)207 208 elif provider == "google":209 llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", temperature=0)210 211 elif provider == "groq":212 # 1. Leer la clave213 key = os.getenv("GROQ_API_KEY")214 215 if not key:216 raise ValueError("GROQ_API_KEY no encontrada.")217 218 # 2. FIX CRÍTICO: Limpiar cualquier redirección inyectada por Hugging Face Space219 os.environ.pop("OPENAI_BASE_URL", None)220 os.environ.pop("OPENAI_API_BASE", None)221 os.environ.pop("OPENAI_API_KEY", None)222 223 # 3. Instanciar ChatGroq con la URL base explícita y un modelo 100% estable224 llm = ChatGroq(225 model="openai/gpt-oss-120b", # Usamos el nombre base más estable de Groq226 api_key=key,227 temperature=0,228 max_tokens=512,229 max_retries=3,230 )231 print("Modelo Groq cargado con URL protegida")232 233 if llm:234 print('Modelo cargado correctamente')235 236 else:237 raise ValueError(238 "Invalid provider. Choose 'huggingface', 'google' or 'groq'."239 )240 241 llm_with_tools = llm.bind_tools(tools)242 243 def assistant(state: MessagesState):244 """Assistant node con control de ventana de contexto para no agotar tokens de Groq."""245 messages = state["messages"]246 247 # Si el historial crece demasiado (más de 6 mensajes en la misma pregunta),248 # preservamos el SystemMessage (messages[0]) y los últimos 4 mensajes.249 #if len(messages) > 10:250 # messages = [messages[0]] + messages[-4:]251 252 return {"messages": [llm_with_tools.invoke(messages)]}253 254 def retriever(state: MessagesState):255 """Retriever node simplified"""256 return {"messages": [sys_msg] + state["messages"]}257 258 builder = StateGraph(MessagesState)259 builder.add_node("retriever", retriever)260 builder.add_node("assistant", assistant)261 builder.add_node("tools", ToolNode(tools))262 263 builder.add_edge(START, "retriever")264 builder.add_edge("retriever", "assistant")265 builder.add_conditional_edges("assistant", tools_condition)266 builder.add_edge("tools", "assistant")267 268 return builder.compile()