Spaces:
Sleeping
Sleeping
SHAMIL SHAHBAZ AWAN
commited on
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,32 +2,31 @@ import datetime
|
|
| 2 |
import json
|
| 3 |
import os
|
| 4 |
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from langchain_core.messages import SystemMessage, HumanMessage
|
| 6 |
from langchain_community.tools.tavily_search import TavilySearchResults
|
| 7 |
from langchain_community.document_loaders import WebBaseLoader
|
| 8 |
from langchain_community.vectorstores import FAISS
|
| 9 |
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 10 |
from langchain.tools.retriever import create_retriever_tool
|
| 11 |
-
from langgraph.graph import StateGraph, START
|
| 12 |
-
from langgraph.checkpoint.memory import MemorySaver
|
| 13 |
-
from langgraph.graph.state import CompiledStateGraph
|
| 14 |
-
from langgraph.graph import MessagesState
|
| 15 |
-
from langgraph.prebuilt import ToolNode
|
| 16 |
-
from langgraph.prebuilt import tools_condition
|
| 17 |
-
from dotenv import load_dotenv
|
| 18 |
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
|
| 19 |
-
from
|
| 20 |
-
from langgraph.graph import
|
| 21 |
-
from
|
| 22 |
-
from
|
| 23 |
-
import
|
| 24 |
-
import streamlit as st
|
| 25 |
import tavily
|
| 26 |
|
| 27 |
# Load environment variables
|
| 28 |
load_dotenv()
|
| 29 |
|
| 30 |
-
# Initialize
|
| 31 |
llm = ChatGoogleGenerativeAI(
|
| 32 |
model="gemini-1.5-flash",
|
| 33 |
google_api_key=os.getenv("GOOGLE_API_KEY")
|
|
@@ -36,77 +35,61 @@ llm = ChatGoogleGenerativeAI(
|
|
| 36 |
def space_events_agent():
|
| 37 |
"""
|
| 38 |
Fetches and formats upcoming space events from NASA's EONET API.
|
| 39 |
-
|
| 40 |
-
Args:
|
| 41 |
-
state (AgentState): The current agent state.
|
| 42 |
-
|
| 43 |
Returns:
|
| 44 |
-
dict: A dictionary with
|
| 45 |
-
|
| 46 |
-
Raises:
|
| 47 |
-
HTTPError: If the request to the NASA API fails.
|
| 48 |
-
|
| 49 |
-
Note:
|
| 50 |
-
Requires NASA API key set in the environment variable `NASA_API_KEY`.
|
| 51 |
"""
|
| 52 |
base_url = "https://eonet.gsfc.nasa.gov/api/v2.1/events"
|
| 53 |
-
# Using a separate NASA API key for EONET if needed
|
| 54 |
params = {"api_key": os.getenv("NASA_API_KEY")}
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
if events:
|
| 59 |
event_details = []
|
| 60 |
for event in events:
|
| 61 |
-
# Safely retrieve category and coordinates
|
| 62 |
category = (event.get('categories', [{}])[0]).get('title', 'Unknown Category')
|
| 63 |
geometries = event.get('geometries', [])
|
| 64 |
coordinates = geometries[0].get('coordinates', 'Unknown Location') if geometries else 'Unknown Location'
|
|
|
|
| 65 |
event_details.append(
|
| 66 |
-
f"• Event
|
| 67 |
-
f" Type
|
| 68 |
-
f" Location
|
| 69 |
)
|
| 70 |
formatted_events = "\n".join(event_details)
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
else:
|
| 73 |
-
return {"output":
|
| 74 |
|
|
|
|
| 75 |
loader1 = WebBaseLoader("https://spacelaunchnow.me/3.3.0/launch/upcoming/")
|
| 76 |
docs1 = loader1.load()
|
| 77 |
-
documents = RecursiveCharacterTextSplitter(
|
| 78 |
-
chunk_size=1000, chunk_overlap=200
|
| 79 |
-
).split_documents(docs1)
|
| 80 |
vector = FAISS.from_documents(documents, GoogleGenerativeAIEmbeddings(model="models/embedding-001"))
|
| 81 |
-
|
| 82 |
retriever = vector.as_retriever()
|
| 83 |
retriever_tool = create_retriever_tool(
|
| 84 |
retriever,
|
| 85 |
"spacelaunchnow",
|
| 86 |
-
"Search for
|
| 87 |
)
|
| 88 |
|
| 89 |
def astronomy_image_agent(user_input: str):
|
| 90 |
"""
|
| 91 |
-
Retrieves astronomy-related images
|
| 92 |
-
|
| 93 |
-
If the user query contains "galaxy", it fetches galaxy images from NASA's Image and Video Library.
|
| 94 |
-
If no specific keyword is found, it defaults to fetching the Astronomy Picture of the Day (APOD).
|
| 95 |
-
|
| 96 |
-
Args:
|
| 97 |
-
user_input (str): The user input containing the query.
|
| 98 |
-
|
| 99 |
Returns:
|
| 100 |
-
dict: A dictionary
|
| 101 |
-
including the title, description, and URL. If an error occurs, an error message is returned.
|
| 102 |
-
|
| 103 |
-
Raises:
|
| 104 |
-
HTTPError: If the request to the NASA API fails.
|
| 105 |
-
ValueError: If no relevant image or data is found.
|
| 106 |
"""
|
| 107 |
user_input = user_input.lower()
|
| 108 |
if "galaxy" in user_input:
|
| 109 |
-
# Query NASA's Image and Video Library API for galaxy images.
|
| 110 |
search_url = "https://images-api.nasa.gov/search"
|
| 111 |
params = {"q": "galaxy", "media_type": "image"}
|
| 112 |
try:
|
|
@@ -121,197 +104,157 @@ def astronomy_image_agent(user_input: str):
|
|
| 121 |
title = first_item.get("data", [{}])[0].get("title", "No Title")
|
| 122 |
description = first_item.get("data", [{}])[0].get("description", "No Description")
|
| 123 |
result = (
|
| 124 |
-
f"Title
|
| 125 |
-
f"Description
|
| 126 |
-
f"Image URL
|
|
|
|
| 127 |
)
|
| 128 |
-
return {"output": f"✨ Galaxy Image Agent
|
| 129 |
else:
|
| 130 |
-
return {"output": "No galaxy images found."}
|
| 131 |
except Exception as e:
|
| 132 |
-
return {"output": f"Galaxy
|
| 133 |
else:
|
| 134 |
-
# Default to
|
| 135 |
url = f"https://api.nasa.gov/planetary/apod?api_key={os.getenv('NASA_API_KEY')}"
|
| 136 |
try:
|
| 137 |
response = requests.get(url)
|
| 138 |
response.raise_for_status()
|
| 139 |
data = response.json()
|
| 140 |
result = (
|
| 141 |
-
f"Title
|
| 142 |
-
f"Date
|
| 143 |
-
f"Explanation
|
| 144 |
-
f"Image URL
|
| 145 |
-
f"Media Type
|
| 146 |
-
f"Copyright
|
|
|
|
| 147 |
)
|
| 148 |
-
return {"output": f"✨ Astronomy Image Agent
|
| 149 |
except Exception as e:
|
| 150 |
-
return {"output": f"Astronomy Image Agent Error: {str(e)}"}
|
| 151 |
|
| 152 |
def educational_resources_agent():
|
| 153 |
"""
|
| 154 |
-
Retrieves and formats a list of high-quality educational resources on astronomy and space science.
|
| 155 |
-
Uses Tavily search to
|
| 156 |
-
|
| 157 |
-
Args:
|
| 158 |
-
state (AgentState): The current agent state, containing user input.
|
| 159 |
-
|
| 160 |
Returns:
|
| 161 |
-
dict: A dictionary containing a list of educational resources
|
| 162 |
-
|
| 163 |
-
Raises:
|
| 164 |
-
Exception: If an error occurs while retrieving the resources or formatting the response.
|
| 165 |
"""
|
| 166 |
try:
|
| 167 |
-
|
| 168 |
-
resources = search.invoke({"query": "Provide a curated list of high-quality educational resources on astronomy and space science, from trusted sources like NASA, ESA, and reputable educational platforms."})
|
| 169 |
if not resources:
|
| 170 |
-
return {"output": "No educational resources found. Please try again."}
|
| 171 |
|
| 172 |
formatted_resources = "\n\n".join(
|
| 173 |
-
[f"📘 {r.get('title', 'Untitled Resource')}
|
| 174 |
)
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
except Exception as e:
|
| 177 |
-
return {"output": f"Educational Resources Agent Error: {str(e)}"}
|
| 178 |
|
| 179 |
def iss_location_agent():
|
| 180 |
"""
|
| 181 |
-
Fetches astronaut data and the current position of the International Space Station (ISS).
|
| 182 |
-
|
| 183 |
-
It fetches the list of astronauts currently on the ISS, and retrieves the current position of the ISS.
|
| 184 |
-
The data is updated at intervals and returned in a structured format.
|
| 185 |
-
|
| 186 |
Returns:
|
| 187 |
-
dict: A dictionary containing astronaut information and ISS position updates.
|
| 188 |
-
Includes a list of people on the ISS and the positions over time.
|
| 189 |
"""
|
| 190 |
# Fetch astronaut data
|
| 191 |
api_url = "http://api.open-notify.org/astros.json"
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
for person in data['people']:
|
| 203 |
-
result["people"].append(f"{person['name']} is currently on the ISS, Craft: {person['craft']}")
|
| 204 |
else:
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
#
|
| 208 |
positions = []
|
| 209 |
-
|
| 210 |
-
# fetching ISS position data
|
| 211 |
-
for attempt in range(5): # Retry 5 times
|
| 212 |
try:
|
| 213 |
-
|
| 214 |
-
if
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
"latitude": latitude,
|
| 222 |
-
"longitude": longitude,
|
| 223 |
-
"time": str(datetime.datetime.fromtimestamp(data['timestamp']))
|
| 224 |
-
}
|
| 225 |
-
positions.append(position)
|
| 226 |
-
break # Successfully fetched data, break the loop
|
| 227 |
except requests.exceptions.RequestException as e:
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
search = TavilySearchResults(tavily_api_key=os.getenv("TAVILY_API_KEY"))
|
| 235 |
|
|
|
|
| 236 |
tools = [search, iss_location_agent, space_events_agent, astronomy_image_agent, educational_resources_agent]
|
| 237 |
llm_with_tools = llm.bind_tools(tools)
|
| 238 |
|
| 239 |
-
#
|
| 240 |
-
sys_msg = SystemMessage(content='''This system is designed to provide real-time astronomical data, visualizations, and educational content. Below are the key functions and tools integrated into the system and their specific purposes:
|
| 241 |
|
| 242 |
1. **Tavily Search (`search`) Integration**:
|
| 243 |
-
- **Purpose**: Provides
|
| 244 |
-
- **Usage**:
|
| 245 |
-
|
| 246 |
-
2. **NASA APOD Tool (`get_nasa_apod_tool`)**:
|
| 247 |
-
- **Purpose**: Fetches the Astronomy Picture of the Day (APOD) from NASA's APOD API.
|
| 248 |
-
- **Usage**: Provides the title, explanation, and image URL of the latest astronomy image shared by NASA, offering users daily insights into the wonders of space.
|
| 249 |
|
| 250 |
-
|
| 251 |
-
- **Purpose**:
|
| 252 |
-
- **Usage**: Provides
|
| 253 |
|
| 254 |
-
|
| 255 |
-
- **Purpose**:
|
| 256 |
-
- **Usage**:
|
| 257 |
|
| 258 |
-
|
| 259 |
-
- **Purpose**:
|
| 260 |
-
- **Usage**:
|
| 261 |
|
| 262 |
-
|
| 263 |
-
- **Purpose**:
|
| 264 |
-
- **Usage**: Provides
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
- **Usage**: Provides information on potentially hazardous asteroids and comets, helping users stay informed about objects that come close to Earth.
|
| 269 |
-
8. **Space News Agent (`retriever_tool`)**:
|
| 270 |
-
- **Purpose**: Fetches the latest space news articles.
|
| 271 |
-
- **Usage**: Provides users with up-to-date news articles related to space exploration, satellite launches, and astronomical discoveries.
|
| 272 |
|
| 273 |
-
|
| 274 |
-
- The system responds dynamically to user queries by identifying the appropriate tool for each request.
|
| 275 |
-
- If the user asks for data or visualizations related to astronomical objects or space events, the relevant NASA APIs or visualization tools are invoked.
|
| 276 |
-
- For general space education or research, tools like Tavily and Space Trivia provide comprehensive, up-to-date educational content.
|
| 277 |
-
- The system remembers the context of conversations to ensure relevant and seamless interactions with users, supported by the `MemorySaver`.
|
| 278 |
-
|
| 279 |
-
### Guidelines:
|
| 280 |
-
- **Scientific Accuracy**: All provided information is scientifically accurate and up-to-date.
|
| 281 |
-
- **Engagement**: Educational content is easy to understand and presented in an interactive and engaging way.
|
| 282 |
-
- **Visualization**: For requests requiring visual representations (such as ISS tracking or NEO data), interactive orbital plots or maps are rendered for clarity.
|
| 283 |
-
- **Tool Alignment**: The system uses the most appropriate tool based on the user’s request, ensuring minimal invocation of unnecessary functions.
|
| 284 |
-
|
| 285 |
-
This setup ensures that the assistant can efficiently address a wide range of user queries related to space, astronomy, and related educational content while keeping interactions intuitive and engaging.''')
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
# Node
|
| 289 |
def assistant(state: MessagesState) -> MessagesState:
|
| 290 |
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"][-10:])]}
|
| 291 |
|
| 292 |
-
# Build graph
|
| 293 |
builder: StateGraph = StateGraph(MessagesState)
|
| 294 |
-
|
| 295 |
-
# Define nodes: these do the work
|
| 296 |
builder.add_node("assistant", assistant)
|
| 297 |
builder.add_node("tools", ToolNode(tools))
|
| 298 |
-
|
| 299 |
-
# Define edges: these determine how the control flow moves
|
| 300 |
builder.add_edge(START, "assistant")
|
| 301 |
-
builder.add_conditional_edges(
|
| 302 |
-
"assistant",
|
| 303 |
-
# If the latest message (result) from assistant is a tool call -> tools_condition routes to tools
|
| 304 |
-
# If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END
|
| 305 |
-
tools_condition,
|
| 306 |
-
)
|
| 307 |
builder.add_edge("tools", "assistant")
|
| 308 |
memory: MemorySaver = MemorySaver()
|
| 309 |
-
# Compile the workflow into an application
|
| 310 |
react_graph_memory: CompiledStateGraph = builder.compile(checkpointer=memory)
|
| 311 |
|
| 312 |
-
|
| 313 |
st.title("🚀 Space Agentic Chatbot")
|
| 314 |
-
st.caption("Your AI-powered space exploration assistant")
|
| 315 |
|
| 316 |
if "messages" not in st.session_state:
|
| 317 |
st.session_state.messages = []
|
|
@@ -319,44 +262,39 @@ if "messages" not in st.session_state:
|
|
| 319 |
# Display existing chat messages
|
| 320 |
for message in st.session_state.messages:
|
| 321 |
with st.chat_message(message["role"]):
|
|
|
|
| 322 |
if message["content"].startswith("http"):
|
| 323 |
st.image(message["content"])
|
| 324 |
else:
|
| 325 |
st.write(message["content"])
|
| 326 |
|
| 327 |
# User input via chat
|
| 328 |
-
if user_input := st.chat_input("Ask about space news, ISS location, or
|
| 329 |
st.session_state.messages.append({"role": "user", "content": user_input})
|
| 330 |
-
st.chat_message("user")
|
| 331 |
-
|
| 332 |
try:
|
| 333 |
-
# Prepare the
|
| 334 |
messages = [HumanMessage(content=user_input)]
|
| 335 |
-
|
| 336 |
-
# Call the memory and invoke the response
|
| 337 |
response = react_graph_memory.invoke({"messages": messages}, config={"configurable": {"thread_id": "1"}})
|
| 338 |
assistant_response = response["messages"][-1].content
|
| 339 |
|
| 340 |
-
#
|
|
|
|
| 341 |
if "image url:" in assistant_response.lower():
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
st.session_state.messages.append({"role": "assistant", "content": assistant_response})
|
| 347 |
-
with st.chat_message("assistant"):
|
| 348 |
-
st.image(image_url) # Display the image
|
| 349 |
-
st.write(assistant_response) # Display the accompanying text
|
| 350 |
-
|
| 351 |
-
else:
|
| 352 |
-
# If no image URL, just store the regular text response
|
| 353 |
-
st.session_state.messages.append({"role": "assistant", "content": assistant_response})
|
| 354 |
-
with st.chat_message("assistant"):
|
| 355 |
-
st.write(assistant_response)
|
| 356 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
except Exception as e:
|
| 358 |
error_msg = f"Error processing request: {str(e)}"
|
| 359 |
-
# Store the error message in session state
|
| 360 |
st.session_state.messages.append({"role": "assistant", "content": error_msg})
|
| 361 |
with st.chat_message("assistant"):
|
| 362 |
st.write(error_msg)
|
|
|
|
| 2 |
import json
|
| 3 |
import os
|
| 4 |
import time
|
| 5 |
+
import re
|
| 6 |
+
import requests
|
| 7 |
+
import streamlit as st
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
from pydantic import BaseModel
|
| 10 |
+
|
| 11 |
+
# LangChain, LangGraph, and related imports
|
| 12 |
from langchain_core.messages import SystemMessage, HumanMessage
|
| 13 |
from langchain_community.tools.tavily_search import TavilySearchResults
|
| 14 |
from langchain_community.document_loaders import WebBaseLoader
|
| 15 |
from langchain_community.vectorstores import FAISS
|
| 16 |
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 17 |
from langchain.tools.retriever import create_retriever_tool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
|
| 19 |
+
from langgraph.graph import StateGraph, START, END
|
| 20 |
+
from langgraph.graph.state import MessagesState
|
| 21 |
+
from langgraph.prebuilt import ToolNode, tools_condition
|
| 22 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 23 |
+
from langgraph.graph import CompiledStateGraph
|
|
|
|
| 24 |
import tavily
|
| 25 |
|
| 26 |
# Load environment variables
|
| 27 |
load_dotenv()
|
| 28 |
|
| 29 |
+
# Initialize the LLM (using Gemini-1.5 Flash) with Google API key from env variables
|
| 30 |
llm = ChatGoogleGenerativeAI(
|
| 31 |
model="gemini-1.5-flash",
|
| 32 |
google_api_key=os.getenv("GOOGLE_API_KEY")
|
|
|
|
| 35 |
def space_events_agent():
|
| 36 |
"""
|
| 37 |
Fetches and formats upcoming space events from NASA's EONET API.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
Returns:
|
| 39 |
+
dict: A dictionary with a detailed formatted message of the current space events.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
"""
|
| 41 |
base_url = "https://eonet.gsfc.nasa.gov/api/v2.1/events"
|
|
|
|
| 42 |
params = {"api_key": os.getenv("NASA_API_KEY")}
|
| 43 |
+
try:
|
| 44 |
+
response = requests.get(base_url, params=params)
|
| 45 |
+
response.raise_for_status()
|
| 46 |
+
events = response.json().get("events", [])
|
| 47 |
+
except Exception as e:
|
| 48 |
+
return {"output": f"🚀 Space Events Agent Error: Unable to fetch events. Details: {str(e)}"}
|
| 49 |
+
|
| 50 |
if events:
|
| 51 |
event_details = []
|
| 52 |
for event in events:
|
|
|
|
| 53 |
category = (event.get('categories', [{}])[0]).get('title', 'Unknown Category')
|
| 54 |
geometries = event.get('geometries', [])
|
| 55 |
coordinates = geometries[0].get('coordinates', 'Unknown Location') if geometries else 'Unknown Location'
|
| 56 |
+
# Include additional detailed formatting for each event
|
| 57 |
event_details.append(
|
| 58 |
+
f"• **Event:** {event.get('title', 'N/A')}\n"
|
| 59 |
+
f" **Type:** {category}\n"
|
| 60 |
+
f" **Location (Lon, Lat):** {coordinates}\n"
|
| 61 |
)
|
| 62 |
formatted_events = "\n".join(event_details)
|
| 63 |
+
detailed_message = (
|
| 64 |
+
f"🚀 **Space Events Agent:**\n\n"
|
| 65 |
+
f"Here are the latest space events with detailed information:\n\n{formatted_events}\n"
|
| 66 |
+
f"*(Data retrieved from NASA's EONET API)*"
|
| 67 |
+
)
|
| 68 |
+
return {"output": detailed_message}
|
| 69 |
else:
|
| 70 |
+
return {"output": "🚀 Space Events Agent: No events found at the moment. Please check back later for updates."}
|
| 71 |
|
| 72 |
+
# Load upcoming launches data and prepare a retriever for space news
|
| 73 |
loader1 = WebBaseLoader("https://spacelaunchnow.me/3.3.0/launch/upcoming/")
|
| 74 |
docs1 = loader1.load()
|
| 75 |
+
documents = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200).split_documents(docs1)
|
|
|
|
|
|
|
| 76 |
vector = FAISS.from_documents(documents, GoogleGenerativeAIEmbeddings(model="models/embedding-001"))
|
|
|
|
| 77 |
retriever = vector.as_retriever()
|
| 78 |
retriever_tool = create_retriever_tool(
|
| 79 |
retriever,
|
| 80 |
"spacelaunchnow",
|
| 81 |
+
"Search for up-to-date space launch news and real time information"
|
| 82 |
)
|
| 83 |
|
| 84 |
def astronomy_image_agent(user_input: str):
|
| 85 |
"""
|
| 86 |
+
Retrieves astronomy-related images. If the query mentions 'galaxy', it fetches an image from
|
| 87 |
+
NASA's Image and Video Library; otherwise it fetches NASA’s Astronomy Picture of the Day (APOD).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
Returns:
|
| 89 |
+
dict: A dictionary with detailed image information including title, explanation, and image URL.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
"""
|
| 91 |
user_input = user_input.lower()
|
| 92 |
if "galaxy" in user_input:
|
|
|
|
| 93 |
search_url = "https://images-api.nasa.gov/search"
|
| 94 |
params = {"q": "galaxy", "media_type": "image"}
|
| 95 |
try:
|
|
|
|
| 104 |
title = first_item.get("data", [{}])[0].get("title", "No Title")
|
| 105 |
description = first_item.get("data", [{}])[0].get("description", "No Description")
|
| 106 |
result = (
|
| 107 |
+
f"**Title:** {title}\n"
|
| 108 |
+
f"**Description:** {description}\n"
|
| 109 |
+
f"**Image URL:** {image_url}\n\n"
|
| 110 |
+
f"This image was retrieved from NASA's Image and Video Library."
|
| 111 |
)
|
| 112 |
+
return {"output": f"✨ **Galaxy Image Agent:**\n\n{result}"}
|
| 113 |
else:
|
| 114 |
+
return {"output": "✨ Galaxy Image Agent: No galaxy images found. Please try another query."}
|
| 115 |
except Exception as e:
|
| 116 |
+
return {"output": f"✨ Galaxy Image Agent Error: {str(e)}"}
|
| 117 |
else:
|
| 118 |
+
# Default to Astronomy Picture of the Day (APOD)
|
| 119 |
url = f"https://api.nasa.gov/planetary/apod?api_key={os.getenv('NASA_API_KEY')}"
|
| 120 |
try:
|
| 121 |
response = requests.get(url)
|
| 122 |
response.raise_for_status()
|
| 123 |
data = response.json()
|
| 124 |
result = (
|
| 125 |
+
f"**Title:** {data.get('title', 'N/A')}\n"
|
| 126 |
+
f"**Date:** {data.get('date', 'N/A')}\n"
|
| 127 |
+
f"**Explanation:** {data.get('explanation', 'N/A')}\n"
|
| 128 |
+
f"**Image URL:** {data.get('url', 'N/A')}\n"
|
| 129 |
+
f"**Media Type:** {data.get('media_type', 'N/A')}\n"
|
| 130 |
+
f"**Copyright:** {data.get('copyright', 'Public Domain')}\n\n"
|
| 131 |
+
f"This Astronomy Picture of the Day is provided by NASA's APOD API and is updated daily."
|
| 132 |
)
|
| 133 |
+
return {"output": f"✨ **Astronomy Image Agent:**\n\n{result}"}
|
| 134 |
except Exception as e:
|
| 135 |
+
return {"output": f"✨ Astronomy Image Agent Error: {str(e)}"}
|
| 136 |
|
| 137 |
def educational_resources_agent():
|
| 138 |
"""
|
| 139 |
+
Retrieves and formats a list of high-quality educational resources on astronomy and space science.
|
| 140 |
+
Uses Tavily search to fetch resources from trusted sources.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
Returns:
|
| 142 |
+
dict: A dictionary containing a detailed list of curated educational resources.
|
|
|
|
|
|
|
|
|
|
| 143 |
"""
|
| 144 |
try:
|
| 145 |
+
resources = search.invoke({"query": "Provide a curated list of high-quality educational resources on astronomy and space science from trusted sources like NASA, ESA, and reputable educational platforms."})
|
|
|
|
| 146 |
if not resources:
|
| 147 |
+
return {"output": "🎓 Educational Resources Agent: No educational resources found. Please try again."}
|
| 148 |
|
| 149 |
formatted_resources = "\n\n".join(
|
| 150 |
+
[f"📘 **{r.get('title', 'Untitled Resource')}**\n🔗 {r.get('url', 'No URL provided')}" for r in resources]
|
| 151 |
)
|
| 152 |
+
detailed_message = (
|
| 153 |
+
f"🎓 **Educational Resources Agent:**\n\n"
|
| 154 |
+
f"Below is a curated list of high-quality educational resources on astronomy and space science. "
|
| 155 |
+
f"Click on the links for more detailed information:\n\n{formatted_resources}"
|
| 156 |
+
)
|
| 157 |
+
return {"output": detailed_message}
|
| 158 |
except Exception as e:
|
| 159 |
+
return {"output": f"🎓 Educational Resources Agent Error: {str(e)}"}
|
| 160 |
|
| 161 |
def iss_location_agent():
|
| 162 |
"""
|
| 163 |
+
Fetches detailed astronaut data and the current position of the International Space Station (ISS).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
Returns:
|
| 165 |
+
dict: A dictionary containing a detailed formatted message with astronaut information and ISS position updates.
|
|
|
|
| 166 |
"""
|
| 167 |
# Fetch astronaut data
|
| 168 |
api_url = "http://api.open-notify.org/astros.json"
|
| 169 |
+
try:
|
| 170 |
+
data = requests.get(api_url).json()
|
| 171 |
+
except Exception as e:
|
| 172 |
+
return {"output": f"🚀 ISS Location Agent Error: Unable to fetch astronaut data. Details: {str(e)}"}
|
| 173 |
+
|
| 174 |
+
output_lines = []
|
| 175 |
+
if data.get('message') == 'success':
|
| 176 |
+
astronauts_line = f"There are currently {data.get('number', 'N/A')} people in space."
|
| 177 |
+
people_lines = "\n".join([f"- {person['name']} aboard {person['craft']}" for person in data.get('people', [])])
|
| 178 |
+
output_lines.append(f"**Astronaut Information:**\n{astronauts_line}\n{people_lines}")
|
|
|
|
|
|
|
| 179 |
else:
|
| 180 |
+
output_lines.append("Error: Failed to obtain astronaut data.")
|
| 181 |
+
|
| 182 |
+
# Fetch ISS current position data with retries
|
| 183 |
positions = []
|
| 184 |
+
for attempt in range(5):
|
|
|
|
|
|
|
| 185 |
try:
|
| 186 |
+
pos_data = requests.get("http://api.open-notify.org/iss-now.json", timeout=10).json()
|
| 187 |
+
if pos_data.get('message') == 'success':
|
| 188 |
+
location = pos_data.get("iss_position", {})
|
| 189 |
+
latitude = float(location.get('latitude', 0))
|
| 190 |
+
longitude = float(location.get('longitude', 0))
|
| 191 |
+
timestamp = datetime.datetime.fromtimestamp(pos_data.get('timestamp', 0)).strftime("%Y-%m-%d %H:%M:%S")
|
| 192 |
+
positions.append(f"- Latitude: {latitude}, Longitude: {longitude} (Recorded at: {timestamp})")
|
| 193 |
+
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
except requests.exceptions.RequestException as e:
|
| 195 |
+
time.sleep(3)
|
| 196 |
+
if positions:
|
| 197 |
+
output_lines.append("**Current ISS Position:**\n" + "\n".join(positions))
|
| 198 |
+
else:
|
| 199 |
+
output_lines.append("Error: Failed to retrieve ISS position data after multiple attempts.")
|
| 200 |
+
|
| 201 |
+
detailed_message = (
|
| 202 |
+
f"🚀 **ISS Location Agent:**\n\n" +
|
| 203 |
+
"\n\n".join(output_lines) +
|
| 204 |
+
"\n\n*(Data retrieved from open-notify.org)*"
|
| 205 |
+
)
|
| 206 |
+
return {"output": detailed_message}
|
| 207 |
+
|
| 208 |
+
# Initialize Tavily search tool using the provided API key
|
| 209 |
search = TavilySearchResults(tavily_api_key=os.getenv("TAVILY_API_KEY"))
|
| 210 |
|
| 211 |
+
# List of tools available to the assistant
|
| 212 |
tools = [search, iss_location_agent, space_events_agent, astronomy_image_agent, educational_resources_agent]
|
| 213 |
llm_with_tools = llm.bind_tools(tools)
|
| 214 |
|
| 215 |
+
# Updated system message instructing detailed and informative responses
|
| 216 |
+
sys_msg = SystemMessage(content='''This system is designed to provide real-time astronomical data, visualizations, and educational content. The assistant must generate responses that are detailed, informative, and rich in context. Below are the key functions and tools integrated into the system and their specific purposes:
|
| 217 |
|
| 218 |
1. **Tavily Search (`search`) Integration**:
|
| 219 |
+
- **Purpose**: Provides up-to-date, relevant space-related information.
|
| 220 |
+
- **Usage**: Fetches the latest news, research articles, and educational resources related to space.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
+
2. **NASA APOD Tool**:
|
| 223 |
+
- **Purpose**: Fetches NASA’s Astronomy Picture of the Day (APOD).
|
| 224 |
+
- **Usage**: Provides detailed information including title, explanation, and image URL.
|
| 225 |
|
| 226 |
+
3. **NASA EONET Space Events**:
|
| 227 |
+
- **Purpose**: Retrieves real-time space events.
|
| 228 |
+
- **Usage**: Provides detailed data on ongoing/upcoming space events.
|
| 229 |
|
| 230 |
+
4. **ISS Location and Astronaut Data**:
|
| 231 |
+
- **Purpose**: Displays the current ISS location and astronaut information.
|
| 232 |
+
- **Usage**: Provides a detailed update on ISS coordinates and people in space.
|
| 233 |
|
| 234 |
+
5. **Space Educational Resources**:
|
| 235 |
+
- **Purpose**: Curates educational resources on astronomy and space science.
|
| 236 |
+
- **Usage**: Provides a detailed list with titles and clickable links.
|
| 237 |
|
| 238 |
+
When responding to user queries, the assistant should provide comprehensive, detailed, and accurate explanations. Visual responses should include image URLs that the interface can use to display pictures. All responses should strive for clarity and depth in explanation.
|
| 239 |
+
''')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
+
# Define the assistant node function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
def assistant(state: MessagesState) -> MessagesState:
|
| 243 |
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"][-10:])]}
|
| 244 |
|
| 245 |
+
# Build the LangGraph state graph
|
| 246 |
builder: StateGraph = StateGraph(MessagesState)
|
|
|
|
|
|
|
| 247 |
builder.add_node("assistant", assistant)
|
| 248 |
builder.add_node("tools", ToolNode(tools))
|
|
|
|
|
|
|
| 249 |
builder.add_edge(START, "assistant")
|
| 250 |
+
builder.add_conditional_edges("assistant", tools_condition)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
builder.add_edge("tools", "assistant")
|
| 252 |
memory: MemorySaver = MemorySaver()
|
|
|
|
| 253 |
react_graph_memory: CompiledStateGraph = builder.compile(checkpointer=memory)
|
| 254 |
|
| 255 |
+
# Streamlit App Layout
|
| 256 |
st.title("🚀 Space Agentic Chatbot")
|
| 257 |
+
st.caption("Your AI-powered space exploration assistant providing detailed and informative insights.")
|
| 258 |
|
| 259 |
if "messages" not in st.session_state:
|
| 260 |
st.session_state.messages = []
|
|
|
|
| 262 |
# Display existing chat messages
|
| 263 |
for message in st.session_state.messages:
|
| 264 |
with st.chat_message(message["role"]):
|
| 265 |
+
# If the message content is a URL (likely an image), display it
|
| 266 |
if message["content"].startswith("http"):
|
| 267 |
st.image(message["content"])
|
| 268 |
else:
|
| 269 |
st.write(message["content"])
|
| 270 |
|
| 271 |
# User input via chat
|
| 272 |
+
if user_input := st.chat_input("Ask about space news, ISS location, astronomy images, or educational content"):
|
| 273 |
st.session_state.messages.append({"role": "user", "content": user_input})
|
| 274 |
+
with st.chat_message("user"):
|
| 275 |
+
st.write(user_input)
|
| 276 |
try:
|
| 277 |
+
# Prepare the message for the model and invoke the state graph
|
| 278 |
messages = [HumanMessage(content=user_input)]
|
|
|
|
|
|
|
| 279 |
response = react_graph_memory.invoke({"messages": messages}, config={"configurable": {"thread_id": "1"}})
|
| 280 |
assistant_response = response["messages"][-1].content
|
| 281 |
|
| 282 |
+
# Check for image URL markers in a case-insensitive way
|
| 283 |
+
image_url = None
|
| 284 |
if "image url:" in assistant_response.lower():
|
| 285 |
+
# Use regex to extract the first URL after "image url:"
|
| 286 |
+
match = re.search(r"[Ii]mage [Uu][Rr][Ll]:\s*(\S+)", assistant_response)
|
| 287 |
+
if match:
|
| 288 |
+
image_url = match.group(1).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
+
# Save and display the assistant's response
|
| 291 |
+
st.session_state.messages.append({"role": "assistant", "content": assistant_response})
|
| 292 |
+
with st.chat_message("assistant"):
|
| 293 |
+
if image_url:
|
| 294 |
+
st.image(image_url) # Display the image
|
| 295 |
+
st.write(assistant_response)
|
| 296 |
except Exception as e:
|
| 297 |
error_msg = f"Error processing request: {str(e)}"
|
|
|
|
| 298 |
st.session_state.messages.append({"role": "assistant", "content": error_msg})
|
| 299 |
with st.chat_message("assistant"):
|
| 300 |
st.write(error_msg)
|