quantalogic 0.35.0__py3-none-any.whl → 0.50.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantalogic/__init__.py +0 -4
- quantalogic/agent.py +603 -363
- quantalogic/agent_config.py +233 -46
- quantalogic/agent_factory.py +34 -22
- quantalogic/coding_agent.py +16 -14
- quantalogic/config.py +2 -1
- quantalogic/console_print_events.py +4 -8
- quantalogic/console_print_token.py +2 -2
- quantalogic/docs_cli.py +15 -10
- quantalogic/event_emitter.py +258 -83
- quantalogic/flow/__init__.py +23 -0
- quantalogic/flow/flow.py +595 -0
- quantalogic/flow/flow_extractor.py +672 -0
- quantalogic/flow/flow_generator.py +89 -0
- quantalogic/flow/flow_manager.py +407 -0
- quantalogic/flow/flow_manager_schema.py +169 -0
- quantalogic/flow/flow_yaml.md +419 -0
- quantalogic/generative_model.py +109 -77
- quantalogic/get_model_info.py +5 -5
- quantalogic/interactive_text_editor.py +100 -73
- quantalogic/main.py +17 -21
- quantalogic/model_info_list.py +3 -3
- quantalogic/model_info_litellm.py +14 -14
- quantalogic/prompts.py +2 -1
- quantalogic/{llm.py → quantlitellm.py} +29 -39
- quantalogic/search_agent.py +4 -4
- quantalogic/server/models.py +4 -1
- quantalogic/task_file_reader.py +5 -5
- quantalogic/task_runner.py +20 -20
- quantalogic/tool_manager.py +10 -21
- quantalogic/tools/__init__.py +98 -68
- quantalogic/tools/composio/composio.py +416 -0
- quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
- quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
- quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
- quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
- quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
- quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
- quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
- quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
- quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
- quantalogic/tools/duckduckgo_search_tool.py +2 -4
- quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
- quantalogic/tools/finance/ccxt_tool.py +373 -0
- quantalogic/tools/finance/finance_llm_tool.py +387 -0
- quantalogic/tools/finance/google_finance.py +192 -0
- quantalogic/tools/finance/market_intelligence_tool.py +520 -0
- quantalogic/tools/finance/technical_analysis_tool.py +491 -0
- quantalogic/tools/finance/tradingview_tool.py +336 -0
- quantalogic/tools/finance/yahoo_finance.py +236 -0
- quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
- quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
- quantalogic/tools/git/clone_repo_tool.py +189 -0
- quantalogic/tools/git/git_operations_tool.py +532 -0
- quantalogic/tools/google_packages/google_news_tool.py +480 -0
- quantalogic/tools/grep_app_tool.py +123 -186
- quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
- quantalogic/tools/jinja_tool.py +6 -10
- quantalogic/tools/language_handlers/__init__.py +22 -9
- quantalogic/tools/list_directory_tool.py +131 -42
- quantalogic/tools/llm_tool.py +45 -15
- quantalogic/tools/llm_vision_tool.py +59 -7
- quantalogic/tools/markitdown_tool.py +17 -5
- quantalogic/tools/nasa_packages/models.py +47 -0
- quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
- quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
- quantalogic/tools/nasa_packages/services.py +82 -0
- quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
- quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
- quantalogic/tools/product_hunt/services.py +63 -0
- quantalogic/tools/rag_tool/__init__.py +48 -0
- quantalogic/tools/rag_tool/document_metadata.py +15 -0
- quantalogic/tools/rag_tool/query_response.py +20 -0
- quantalogic/tools/rag_tool/rag_tool.py +566 -0
- quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
- quantalogic/tools/read_html_tool.py +24 -38
- quantalogic/tools/replace_in_file_tool.py +10 -10
- quantalogic/tools/safe_python_interpreter_tool.py +10 -24
- quantalogic/tools/search_definition_names.py +2 -2
- quantalogic/tools/sequence_tool.py +14 -23
- quantalogic/tools/sql_query_tool.py +17 -19
- quantalogic/tools/tool.py +39 -15
- quantalogic/tools/unified_diff_tool.py +1 -1
- quantalogic/tools/utilities/csv_processor_tool.py +234 -0
- quantalogic/tools/utilities/download_file_tool.py +179 -0
- quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
- quantalogic/tools/utils/__init__.py +1 -4
- quantalogic/tools/utils/create_sample_database.py +24 -38
- quantalogic/tools/utils/generate_database_report.py +74 -82
- quantalogic/tools/wikipedia_search_tool.py +17 -21
- quantalogic/utils/ask_user_validation.py +1 -1
- quantalogic/utils/async_utils.py +35 -0
- quantalogic/utils/check_version.py +3 -5
- quantalogic/utils/get_all_models.py +2 -1
- quantalogic/utils/git_ls.py +21 -7
- quantalogic/utils/lm_studio_model_info.py +9 -7
- quantalogic/utils/python_interpreter.py +113 -43
- quantalogic/utils/xml_utility.py +178 -0
- quantalogic/version_check.py +1 -1
- quantalogic/welcome_message.py +7 -7
- quantalogic/xml_parser.py +0 -1
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/METADATA +40 -1
- quantalogic-0.50.0.dist-info/RECORD +148 -0
- quantalogic-0.35.0.dist-info/RECORD +0 -102
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,7 @@
|
|
1
1
|
"""LLM Vision Tool for analyzing images using a language model."""
|
2
2
|
|
3
|
-
|
3
|
+
import asyncio
|
4
|
+
from typing import Callable, Optional
|
4
5
|
|
5
6
|
from loguru import logger
|
6
7
|
from pydantic import ConfigDict, Field
|
@@ -59,8 +60,37 @@ class LLMVisionTool(Tool):
|
|
59
60
|
)
|
60
61
|
|
61
62
|
model_name: str = Field(..., description="The name of the language model to use")
|
63
|
+
on_token: Callable | None = Field(default=None, exclude=True)
|
62
64
|
generative_model: Optional[GenerativeModel] = Field(default=None)
|
63
65
|
|
66
|
+
def __init__(
|
67
|
+
self,
|
68
|
+
model_name: str,
|
69
|
+
on_token: Callable | None = None,
|
70
|
+
name: str = "llm_vision_tool",
|
71
|
+
generative_model: GenerativeModel | None = None,
|
72
|
+
):
|
73
|
+
"""Initialize the LLMVisionTool with model configuration and optional callback.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
model_name (str): The name of the language model to use.
|
77
|
+
on_token (Callable, optional): Callback function for streaming tokens.
|
78
|
+
name (str): Name of the tool instance. Defaults to "llm_vision_tool".
|
79
|
+
generative_model (GenerativeModel, optional): Pre-initialized generative model.
|
80
|
+
"""
|
81
|
+
# Use dict to pass validated data to parent constructor
|
82
|
+
super().__init__(
|
83
|
+
**{
|
84
|
+
"model_name": model_name,
|
85
|
+
"on_token": on_token,
|
86
|
+
"name": name,
|
87
|
+
"generative_model": generative_model,
|
88
|
+
}
|
89
|
+
)
|
90
|
+
|
91
|
+
# Initialize the generative model
|
92
|
+
self.model_post_init(None)
|
93
|
+
|
64
94
|
def model_post_init(self, __context):
|
65
95
|
"""Initialize the generative model after model initialization."""
|
66
96
|
if self.generative_model is None:
|
@@ -75,6 +105,30 @@ class LLMVisionTool(Tool):
|
|
75
105
|
def execute(self, system_prompt: str, prompt: str, image_url: str, temperature: str = "0.7") -> str:
|
76
106
|
"""Execute the tool to analyze an image and generate a response.
|
77
107
|
|
108
|
+
Args:
|
109
|
+
system_prompt: The system prompt to guide the model
|
110
|
+
prompt: The question or instruction about the image
|
111
|
+
image_url: URL of the image to analyze
|
112
|
+
temperature: Sampling temperature
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
The generated response
|
116
|
+
|
117
|
+
Raises:
|
118
|
+
ValueError: If temperature is invalid or image_url is malformed
|
119
|
+
Exception: If there's an error during response generation
|
120
|
+
"""
|
121
|
+
# Run the async version synchronously
|
122
|
+
return asyncio.run(
|
123
|
+
self.async_execute(system_prompt=system_prompt, prompt=prompt, image_url=image_url, temperature=temperature)
|
124
|
+
)
|
125
|
+
|
126
|
+
async def async_execute(self, system_prompt: str, prompt: str, image_url: str, temperature: str = "0.7") -> str:
|
127
|
+
"""Execute the tool to analyze an image and generate a response asynchronously.
|
128
|
+
|
129
|
+
This method provides a native asynchronous implementation, utilizing the generative model's
|
130
|
+
asynchronous capabilities for improved performance in async contexts.
|
131
|
+
|
78
132
|
Args:
|
79
133
|
system_prompt: The system prompt to guide the model
|
80
134
|
prompt: The question or instruction about the image
|
@@ -111,17 +165,15 @@ class LLMVisionTool(Tool):
|
|
111
165
|
|
112
166
|
try:
|
113
167
|
is_streaming = self.on_token is not None
|
114
|
-
response_stats = self.generative_model.
|
115
|
-
messages_history=messages_history,
|
116
|
-
prompt=prompt,
|
117
|
-
image_url=image_url,
|
118
|
-
streaming=is_streaming
|
168
|
+
response_stats = await self.generative_model.async_generate_with_history(
|
169
|
+
messages_history=messages_history, prompt=prompt, image_url=image_url, streaming=is_streaming
|
119
170
|
)
|
120
171
|
|
121
172
|
if is_streaming:
|
122
173
|
response = ""
|
123
|
-
for chunk in response_stats:
|
174
|
+
async for chunk in response_stats:
|
124
175
|
response += chunk
|
176
|
+
# Note: on_token is handled via the event emitter set in model_post_init
|
125
177
|
else:
|
126
178
|
response = response_stats.response.strip()
|
127
179
|
|
@@ -65,25 +65,37 @@ class MarkitdownTool(Tool):
|
|
65
65
|
file_path = os.path.expanduser(file_path)
|
66
66
|
if not os.path.isabs(file_path):
|
67
67
|
file_path = os.path.abspath(file_path)
|
68
|
-
|
68
|
+
|
69
69
|
# Verify file exists
|
70
70
|
if not os.path.exists(file_path):
|
71
71
|
return f"Error: File not found at path: {file_path}"
|
72
72
|
|
73
73
|
from markitdown import MarkItDown
|
74
|
+
|
74
75
|
md = MarkItDown()
|
75
|
-
|
76
|
+
|
77
|
+
# Detect file type if possible
|
78
|
+
file_extension = os.path.splitext(file_path)[1].lower()
|
79
|
+
supported_extensions = [".pdf", ".pptx", ".docx", ".xlsx", ".html", ".htm"]
|
80
|
+
|
81
|
+
if not file_extension or file_extension not in supported_extensions:
|
82
|
+
return f"Error: Unsupported file format. Supported formats are: {', '.join(supported_extensions)}"
|
83
|
+
|
84
|
+
try:
|
85
|
+
result = md.convert(file_path)
|
86
|
+
except Exception as e:
|
87
|
+
return f"Error converting file to Markdown: {str(e)}"
|
76
88
|
|
77
89
|
if output_file_path:
|
78
90
|
# Ensure output directory exists
|
79
91
|
output_dir = os.path.dirname(output_file_path)
|
80
92
|
if output_dir and not os.path.exists(output_dir):
|
81
93
|
os.makedirs(output_dir)
|
82
|
-
|
94
|
+
|
83
95
|
with open(output_file_path, "w", encoding="utf-8") as f:
|
84
96
|
f.write(result.text_content)
|
85
97
|
return f"Markdown content successfully written to {output_file_path}"
|
86
|
-
|
98
|
+
|
87
99
|
# Handle content truncation
|
88
100
|
lines = result.text_content.splitlines()
|
89
101
|
if len(lines) > MAX_LINES:
|
@@ -92,7 +104,7 @@ class MarkitdownTool(Tool):
|
|
92
104
|
return result.text_content
|
93
105
|
|
94
106
|
except Exception as e:
|
95
|
-
return f"Error
|
107
|
+
return f"Error processing file: {str(e)}"
|
96
108
|
finally:
|
97
109
|
if is_temp_file and os.path.exists(file_path):
|
98
110
|
os.remove(file_path)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
"""Data models for NASA API responses."""
|
2
|
+
|
3
|
+
from typing import List
|
4
|
+
|
5
|
+
from pydantic import BaseModel, Field
|
6
|
+
|
7
|
+
|
8
|
+
class AsteroidDiameter(BaseModel):
|
9
|
+
"""Asteroid diameter estimates in meters."""
|
10
|
+
min_diameter: float = Field(..., alias="estimated_diameter_min")
|
11
|
+
max_diameter: float = Field(..., alias="estimated_diameter_max")
|
12
|
+
|
13
|
+
class CloseApproach(BaseModel):
|
14
|
+
"""Close approach data for an asteroid."""
|
15
|
+
date: str = Field(..., alias="close_approach_date_full")
|
16
|
+
velocity_kph: float = Field(..., alias="relative_velocity.kilometers_per_hour")
|
17
|
+
miss_distance_km: float = Field(..., alias="miss_distance.kilometers")
|
18
|
+
|
19
|
+
class Asteroid(BaseModel):
|
20
|
+
"""Model for asteroid data."""
|
21
|
+
name: str
|
22
|
+
nasa_jpl_url: str
|
23
|
+
is_hazardous: bool = Field(..., alias="is_potentially_hazardous_asteroid")
|
24
|
+
diameter: AsteroidDiameter = Field(..., alias="estimated_diameter.meters")
|
25
|
+
approaches: List[CloseApproach] = Field(..., alias="close_approach_data")
|
26
|
+
|
27
|
+
def format_info(self) -> str:
|
28
|
+
"""Format asteroid information into readable text."""
|
29
|
+
lines = [
|
30
|
+
f"Name: {self.name}",
|
31
|
+
f"NASA JPL URL: {self.nasa_jpl_url}",
|
32
|
+
f"Potentially Hazardous: {self.is_hazardous}",
|
33
|
+
f"Estimated Diameter:",
|
34
|
+
f" Min: {self.diameter.min_diameter:.2f} meters",
|
35
|
+
f" Max: {self.diameter.max_diameter:.2f} meters"
|
36
|
+
]
|
37
|
+
|
38
|
+
if self.approaches:
|
39
|
+
approach = self.approaches[0]
|
40
|
+
lines.extend([
|
41
|
+
"\nClosest Approach:",
|
42
|
+
f" Date: {approach.date}",
|
43
|
+
f" Velocity: {approach.velocity_kph} km/h",
|
44
|
+
f" Miss Distance: {approach.miss_distance_km} km"
|
45
|
+
])
|
46
|
+
|
47
|
+
return "\n".join(lines)
|
@@ -0,0 +1,232 @@
|
|
1
|
+
"""NASA APOD (Astronomy Picture of the Day) API tool.
|
2
|
+
|
3
|
+
This tool provides access to NASA's APOD API to retrieve astronomy pictures and related information.
|
4
|
+
It supports fetching both current and historical astronomy pictures with detailed metadata.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
from datetime import datetime
|
9
|
+
from typing import Any, Dict, List
|
10
|
+
|
11
|
+
import requests
|
12
|
+
from loguru import logger
|
13
|
+
|
14
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
15
|
+
|
16
|
+
|
17
|
+
class NasaApodTool(Tool):
|
18
|
+
"""Tool for accessing NASA's Astronomy Picture of the Day (APOD) API.
|
19
|
+
|
20
|
+
Features:
|
21
|
+
- Fetch today's astronomy picture
|
22
|
+
- Retrieve pictures from specific dates
|
23
|
+
- Get random astronomy pictures
|
24
|
+
- Support for HD images
|
25
|
+
- Detailed metadata including explanation and copyright info
|
26
|
+
"""
|
27
|
+
|
28
|
+
name: str = "nasa_apod_tool"
|
29
|
+
description: str = (
|
30
|
+
"NASA Astronomy Picture of the Day (APOD) API tool for retrieving astronomy pictures "
|
31
|
+
"and their detailed information including scientific explanations."
|
32
|
+
)
|
33
|
+
arguments: List[ToolArgument] = [
|
34
|
+
ToolArgument(
|
35
|
+
name="date",
|
36
|
+
arg_type="string",
|
37
|
+
description="The date of the APOD image to retrieve (YYYY-MM-DD format)",
|
38
|
+
required=False,
|
39
|
+
default="today",
|
40
|
+
example="2024-01-15",
|
41
|
+
),
|
42
|
+
ToolArgument(
|
43
|
+
name="start_date",
|
44
|
+
arg_type="string",
|
45
|
+
description="Start date for date range search (YYYY-MM-DD format)",
|
46
|
+
required=False,
|
47
|
+
example="2024-01-01",
|
48
|
+
),
|
49
|
+
ToolArgument(
|
50
|
+
name="end_date",
|
51
|
+
arg_type="string",
|
52
|
+
description="End date for date range search (YYYY-MM-DD format)",
|
53
|
+
required=False,
|
54
|
+
example="2024-01-07",
|
55
|
+
),
|
56
|
+
ToolArgument(
|
57
|
+
name="count",
|
58
|
+
arg_type="int",
|
59
|
+
description="Number of random images to return (1-100)",
|
60
|
+
required=False,
|
61
|
+
default="1",
|
62
|
+
example="5",
|
63
|
+
),
|
64
|
+
ToolArgument(
|
65
|
+
name="thumbs",
|
66
|
+
arg_type="boolean",
|
67
|
+
description="Return thumbnail URLs for video content",
|
68
|
+
required=False,
|
69
|
+
default="True",
|
70
|
+
example="True",
|
71
|
+
),
|
72
|
+
]
|
73
|
+
|
74
|
+
def __init__(self):
|
75
|
+
"""Initialize the NASA APOD tool."""
|
76
|
+
super().__init__()
|
77
|
+
self.api_key = os.getenv("LANAZA_API_KEY", "DEMO_KEY")
|
78
|
+
self.base_url = "https://api.nasa.gov/planetary/apod"
|
79
|
+
|
80
|
+
def _fetch_apod_data(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
81
|
+
"""Fetch data from NASA APOD API.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
params: Query parameters for the API request
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
API response data as dictionary
|
88
|
+
|
89
|
+
Raises:
|
90
|
+
RuntimeError: If the API request fails
|
91
|
+
"""
|
92
|
+
try:
|
93
|
+
params["api_key"] = self.api_key
|
94
|
+
response = requests.get(self.base_url, params=params)
|
95
|
+
if response.status_code == 200:
|
96
|
+
return response.json()
|
97
|
+
else:
|
98
|
+
raise RuntimeError(f"API request failed with status {response.status_code}: {response.text}")
|
99
|
+
except Exception as e:
|
100
|
+
raise RuntimeError(f"Failed to fetch APOD data: {str(e)}")
|
101
|
+
|
102
|
+
def _validate_date(self, date_str: str) -> str:
|
103
|
+
"""Validate and format date string.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
date_str: Date string to validate
|
107
|
+
|
108
|
+
Returns:
|
109
|
+
Validated date string in YYYY-MM-DD format
|
110
|
+
|
111
|
+
Raises:
|
112
|
+
ValueError: If date format is invalid
|
113
|
+
"""
|
114
|
+
if date_str.lower() == "today":
|
115
|
+
return datetime.now().strftime("%Y-%m-%d")
|
116
|
+
try:
|
117
|
+
datetime.strptime(date_str, "%Y-%m-%d")
|
118
|
+
return date_str
|
119
|
+
except ValueError:
|
120
|
+
raise ValueError("Invalid date format. Use YYYY-MM-DD format.")
|
121
|
+
|
122
|
+
def _format_apod_result(self, data: Dict[str, Any] | List[Dict[str, Any]]) -> str:
|
123
|
+
"""Format APOD data into readable text.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
data: APOD data from API
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
Formatted string with APOD information
|
130
|
+
"""
|
131
|
+
if isinstance(data, list):
|
132
|
+
return self._format_multiple_results(data)
|
133
|
+
|
134
|
+
result = []
|
135
|
+
result.append(f"Title: {data.get('title', 'N/A')}")
|
136
|
+
result.append(f"Date: {data.get('date', 'N/A')}")
|
137
|
+
if 'copyright' in data:
|
138
|
+
result.append(f"Copyright: {data['copyright']}")
|
139
|
+
|
140
|
+
# Add media links
|
141
|
+
if data.get('media_type') == 'video':
|
142
|
+
result.append(f"Video URL: {data.get('url', 'N/A')}")
|
143
|
+
if data.get('thumbnail_url'):
|
144
|
+
result.append(f"Thumbnail URL: {data.get('thumbnail_url')}")
|
145
|
+
else:
|
146
|
+
result.append(f"Image URL: {data.get('url', 'N/A')}")
|
147
|
+
if data.get('hdurl'):
|
148
|
+
result.append(f"HD Image URL: {data.get('hdurl')}")
|
149
|
+
|
150
|
+
# Add explanation
|
151
|
+
if data.get('explanation'):
|
152
|
+
result.append("\nExplanation:")
|
153
|
+
result.append(data['explanation'])
|
154
|
+
|
155
|
+
return "\n".join(result)
|
156
|
+
|
157
|
+
def _format_multiple_results(self, data_list: List[Dict[str, Any]]) -> str:
|
158
|
+
"""Format multiple APOD results.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
data_list: List of APOD data entries
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
Formatted string with multiple APOD entries
|
165
|
+
"""
|
166
|
+
results = []
|
167
|
+
for i, data in enumerate(data_list, 1):
|
168
|
+
results.append(f"\n--- APOD Entry {i} ---")
|
169
|
+
results.append(self._format_apod_result(data))
|
170
|
+
return "\n".join(results)
|
171
|
+
|
172
|
+
def execute(
|
173
|
+
self,
|
174
|
+
date: str = "today",
|
175
|
+
start_date: str = None,
|
176
|
+
end_date: str = None,
|
177
|
+
count: int = 1,
|
178
|
+
thumbs: bool = True,
|
179
|
+
) -> str:
|
180
|
+
"""Execute APOD API request with specified parameters.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
date: Specific date for APOD image (YYYY-MM-DD)
|
184
|
+
start_date: Start date for date range
|
185
|
+
end_date: End date for date range
|
186
|
+
count: Number of random images to return
|
187
|
+
thumbs: Include thumbnails for video content
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
Formatted string containing APOD data
|
191
|
+
|
192
|
+
Raises:
|
193
|
+
ValueError: If parameters are invalid
|
194
|
+
RuntimeError: If the API request fails
|
195
|
+
"""
|
196
|
+
# Validate parameters
|
197
|
+
params = {"thumbs": str(thumbs).lower()}
|
198
|
+
|
199
|
+
# Handle different query types
|
200
|
+
if start_date and end_date:
|
201
|
+
params["start_date"] = self._validate_date(start_date)
|
202
|
+
params["end_date"] = self._validate_date(end_date)
|
203
|
+
elif count > 1:
|
204
|
+
params["count"] = min(100, max(1, count)) # Ensure count is between 1 and 100
|
205
|
+
else:
|
206
|
+
params["date"] = self._validate_date(date)
|
207
|
+
|
208
|
+
try:
|
209
|
+
data = self._fetch_apod_data(params)
|
210
|
+
return self._format_apod_result(data)
|
211
|
+
except Exception as e:
|
212
|
+
logger.error(f"Error executing APOD tool: {str(e)}")
|
213
|
+
raise
|
214
|
+
|
215
|
+
if __name__ == "__main__":
|
216
|
+
# Example usage
|
217
|
+
tool = NasaApodTool()
|
218
|
+
|
219
|
+
# Test with today's APOD
|
220
|
+
result = tool.execute()
|
221
|
+
print("Today's APOD:")
|
222
|
+
print(result)
|
223
|
+
|
224
|
+
# Test with specific date
|
225
|
+
result = tool.execute(date="2024-01-01")
|
226
|
+
print("\nAPOD for 2024-01-01:")
|
227
|
+
print(result)
|
228
|
+
|
229
|
+
# Test with multiple random images
|
230
|
+
result = tool.execute(count=2)
|
231
|
+
print("\nTwo random APODs:")
|
232
|
+
print(result)
|
@@ -0,0 +1,147 @@
|
|
1
|
+
"""NASA NeoWs (Near Earth Object Web Service) API tool.
|
2
|
+
|
3
|
+
This tool provides access to NASA's NeoWs API to retrieve information about near-Earth asteroids.
|
4
|
+
It supports searching by date ranges, looking up specific asteroids, and browsing the overall dataset.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
import os
|
9
|
+
from typing import List
|
10
|
+
|
11
|
+
from loguru import logger
|
12
|
+
|
13
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
14
|
+
|
15
|
+
from .models import Asteroid
|
16
|
+
from .services import NasaApiService, NeoWsService
|
17
|
+
|
18
|
+
|
19
|
+
class NasaNeoWsTool(Tool):
|
20
|
+
"""Tool for accessing NASA's Near Earth Object Web Service (NeoWs).
|
21
|
+
|
22
|
+
Features:
|
23
|
+
- Search asteroids by date range
|
24
|
+
- Lookup specific asteroids by ID
|
25
|
+
- Browse the overall asteroid dataset
|
26
|
+
"""
|
27
|
+
|
28
|
+
name: str = "nasa_neows_tool"
|
29
|
+
description: str = (
|
30
|
+
"NASA Near Earth Object Web Service (NeoWs) API tool for retrieving information "
|
31
|
+
"about near-Earth asteroids, including their orbits, sizes, and approach distances."
|
32
|
+
)
|
33
|
+
arguments: List[ToolArgument] = [
|
34
|
+
ToolArgument(
|
35
|
+
name="operation",
|
36
|
+
arg_type="string",
|
37
|
+
description="Operation to perform (feed, lookup, browse)",
|
38
|
+
required=True,
|
39
|
+
default="feed",
|
40
|
+
example="feed",
|
41
|
+
),
|
42
|
+
ToolArgument(
|
43
|
+
name="start_date",
|
44
|
+
arg_type="string",
|
45
|
+
description="Start date for asteroid search (YYYY-MM-DD format)",
|
46
|
+
required=False,
|
47
|
+
example="2024-01-01",
|
48
|
+
),
|
49
|
+
ToolArgument(
|
50
|
+
name="end_date",
|
51
|
+
arg_type="string",
|
52
|
+
description="End date for asteroid search (YYYY-MM-DD format)",
|
53
|
+
required=False,
|
54
|
+
example="2024-01-07",
|
55
|
+
),
|
56
|
+
ToolArgument(
|
57
|
+
name="asteroid_id",
|
58
|
+
arg_type="string",
|
59
|
+
description="NASA JPL small body ID for asteroid lookup",
|
60
|
+
required=False,
|
61
|
+
example="3542519",
|
62
|
+
),
|
63
|
+
]
|
64
|
+
|
65
|
+
def __init__(self):
|
66
|
+
"""Initialize the NASA NeoWs tool."""
|
67
|
+
super().__init__()
|
68
|
+
api_service = NasaApiService(os.getenv("LANAZA_API_KEY", "DEMO_KEY"))
|
69
|
+
self.service = NeoWsService(api_service)
|
70
|
+
|
71
|
+
def _format_feed_results(self, data: dict) -> str:
|
72
|
+
"""Format feed results into readable text."""
|
73
|
+
result = [f"Element Count: {data.get('element_count', 0)} asteroids found"]
|
74
|
+
|
75
|
+
for date, asteroids in data.get('near_earth_objects', {}).items():
|
76
|
+
result.extend([
|
77
|
+
f"\nDate: {date}",
|
78
|
+
f"Number of asteroids: {len(asteroids)}"
|
79
|
+
])
|
80
|
+
|
81
|
+
for ast_data in asteroids:
|
82
|
+
asteroid = Asteroid(**ast_data)
|
83
|
+
result.extend([
|
84
|
+
"\n" + asteroid.format_info(),
|
85
|
+
"-" * 50
|
86
|
+
])
|
87
|
+
|
88
|
+
return "\n".join(result)
|
89
|
+
|
90
|
+
async def execute(
|
91
|
+
self,
|
92
|
+
operation: str = "feed",
|
93
|
+
start_date: str = None,
|
94
|
+
end_date: str = None,
|
95
|
+
asteroid_id: str = None,
|
96
|
+
) -> str:
|
97
|
+
"""Execute NeoWs API request with specified parameters."""
|
98
|
+
try:
|
99
|
+
if operation == "feed":
|
100
|
+
data = await self.service.get_feed(start_date, end_date)
|
101
|
+
return self._format_feed_results(data)
|
102
|
+
|
103
|
+
elif operation == "lookup":
|
104
|
+
if not asteroid_id:
|
105
|
+
raise ValueError("asteroid_id is required for lookup operation")
|
106
|
+
data = await self.service.lookup_asteroid(asteroid_id)
|
107
|
+
return Asteroid(**data).format_info()
|
108
|
+
|
109
|
+
elif operation == "browse":
|
110
|
+
data = await self.service.browse_asteroids()
|
111
|
+
result = ["Browse Results:"]
|
112
|
+
for ast_data in data.get('near_earth_objects', []):
|
113
|
+
asteroid = Asteroid(**ast_data)
|
114
|
+
result.extend([
|
115
|
+
"\n" + asteroid.format_info(),
|
116
|
+
"-" * 50
|
117
|
+
])
|
118
|
+
return "\n".join(result)
|
119
|
+
|
120
|
+
else:
|
121
|
+
raise ValueError(f"Invalid operation: {operation}. Must be one of: feed, lookup, browse")
|
122
|
+
|
123
|
+
except Exception as e:
|
124
|
+
logger.error(f"Error executing NeoWs tool: {str(e)}")
|
125
|
+
raise
|
126
|
+
|
127
|
+
if __name__ == "__main__":
|
128
|
+
# Example usage
|
129
|
+
tool = NasaNeoWsTool()
|
130
|
+
|
131
|
+
async def test_tool():
|
132
|
+
# Test feed operation
|
133
|
+
print("Testing Feed Operation:")
|
134
|
+
result = await tool.execute(operation="feed")
|
135
|
+
print(result)
|
136
|
+
|
137
|
+
# Test lookup operation
|
138
|
+
print("\nTesting Lookup Operation:")
|
139
|
+
result = await tool.execute(operation="lookup", asteroid_id="3542519")
|
140
|
+
print(result)
|
141
|
+
|
142
|
+
# Test browse operation
|
143
|
+
print("\nTesting Browse Operation:")
|
144
|
+
result = await tool.execute(operation="browse")
|
145
|
+
print(result)
|
146
|
+
|
147
|
+
asyncio.run(test_tool())
|
@@ -0,0 +1,82 @@
|
|
1
|
+
"""Services for interacting with NASA APIs."""
|
2
|
+
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
from typing import Any, Dict, List
|
5
|
+
|
6
|
+
import aiohttp
|
7
|
+
from loguru import logger
|
8
|
+
|
9
|
+
|
10
|
+
class NasaApiService:
|
11
|
+
"""Service for making NASA API requests."""
|
12
|
+
|
13
|
+
def __init__(self, api_key: str):
|
14
|
+
"""Initialize with API key."""
|
15
|
+
self.api_key = api_key
|
16
|
+
self.base_url = "https://api.nasa.gov/neo/rest/v1"
|
17
|
+
|
18
|
+
async def fetch_data(self, endpoint: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
19
|
+
"""Make an API request to NASA endpoints.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
endpoint: API endpoint path
|
23
|
+
params: Optional query parameters
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
API response data
|
27
|
+
|
28
|
+
Raises:
|
29
|
+
RuntimeError: If request fails
|
30
|
+
"""
|
31
|
+
params = params or {}
|
32
|
+
params["api_key"] = self.api_key
|
33
|
+
url = f"{self.base_url}/{endpoint}"
|
34
|
+
|
35
|
+
try:
|
36
|
+
async with aiohttp.ClientSession() as session:
|
37
|
+
async with session.get(url, params=params) as response:
|
38
|
+
if response.status == 200:
|
39
|
+
return await response.json()
|
40
|
+
error_text = await response.text()
|
41
|
+
raise RuntimeError(f"API error {response.status}: {error_text}")
|
42
|
+
except Exception as e:
|
43
|
+
logger.error(f"NASA API request failed: {str(e)}")
|
44
|
+
raise RuntimeError(f"Failed to fetch data: {str(e)}")
|
45
|
+
|
46
|
+
class NeoWsService:
|
47
|
+
"""Service for Near Earth Object Web Service operations."""
|
48
|
+
|
49
|
+
def __init__(self, api_service: NasaApiService):
|
50
|
+
"""Initialize with API service."""
|
51
|
+
self.api = api_service
|
52
|
+
|
53
|
+
@staticmethod
|
54
|
+
def validate_date(date_str: str) -> str:
|
55
|
+
"""Validate date string format."""
|
56
|
+
try:
|
57
|
+
datetime.strptime(date_str, "%Y-%m-%d")
|
58
|
+
return date_str
|
59
|
+
except ValueError:
|
60
|
+
raise ValueError("Invalid date format. Use YYYY-MM-DD")
|
61
|
+
|
62
|
+
async def get_feed(self, start_date: str, end_date: str = None) -> Dict[str, Any]:
|
63
|
+
"""Get asteroid feed for date range."""
|
64
|
+
start_date = self.validate_date(start_date)
|
65
|
+
if not end_date:
|
66
|
+
end_date = (datetime.strptime(start_date, "%Y-%m-%d") +
|
67
|
+
timedelta(days=7)).strftime("%Y-%m-%d")
|
68
|
+
else:
|
69
|
+
end_date = self.validate_date(end_date)
|
70
|
+
|
71
|
+
return await self.api.fetch_data("feed", {
|
72
|
+
"start_date": start_date,
|
73
|
+
"end_date": end_date
|
74
|
+
})
|
75
|
+
|
76
|
+
async def lookup_asteroid(self, asteroid_id: str) -> Dict[str, Any]:
|
77
|
+
"""Look up specific asteroid by ID."""
|
78
|
+
return await self.api.fetch_data(f"neo/{asteroid_id}")
|
79
|
+
|
80
|
+
async def browse_asteroids(self) -> Dict[str, Any]:
|
81
|
+
"""Browse asteroid dataset."""
|
82
|
+
return await self.api.fetch_data("neo/browse")
|