quantalogic 0.33.4__py3-none-any.whl → 0.40.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.
Files changed (107) hide show
  1. quantalogic/__init__.py +0 -4
  2. quantalogic/agent.py +603 -362
  3. quantalogic/agent_config.py +260 -28
  4. quantalogic/agent_factory.py +43 -17
  5. quantalogic/coding_agent.py +20 -12
  6. quantalogic/config.py +7 -4
  7. quantalogic/console_print_events.py +4 -8
  8. quantalogic/console_print_token.py +2 -2
  9. quantalogic/docs_cli.py +15 -10
  10. quantalogic/event_emitter.py +258 -83
  11. quantalogic/flow/__init__.py +23 -0
  12. quantalogic/flow/flow.py +595 -0
  13. quantalogic/flow/flow_extractor.py +672 -0
  14. quantalogic/flow/flow_generator.py +89 -0
  15. quantalogic/flow/flow_manager.py +407 -0
  16. quantalogic/flow/flow_manager_schema.py +169 -0
  17. quantalogic/flow/flow_yaml.md +419 -0
  18. quantalogic/generative_model.py +109 -77
  19. quantalogic/get_model_info.py +6 -6
  20. quantalogic/interactive_text_editor.py +100 -73
  21. quantalogic/main.py +36 -23
  22. quantalogic/model_info_list.py +12 -0
  23. quantalogic/model_info_litellm.py +14 -14
  24. quantalogic/prompts.py +2 -1
  25. quantalogic/{llm.py → quantlitellm.py} +29 -39
  26. quantalogic/search_agent.py +4 -4
  27. quantalogic/server/models.py +4 -1
  28. quantalogic/task_file_reader.py +5 -5
  29. quantalogic/task_runner.py +21 -20
  30. quantalogic/tool_manager.py +10 -21
  31. quantalogic/tools/__init__.py +98 -68
  32. quantalogic/tools/composio/composio.py +416 -0
  33. quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
  34. quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
  35. quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
  36. quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
  37. quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
  38. quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
  39. quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
  40. quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
  41. quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
  42. quantalogic/tools/duckduckgo_search_tool.py +2 -4
  43. quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
  44. quantalogic/tools/finance/ccxt_tool.py +373 -0
  45. quantalogic/tools/finance/finance_llm_tool.py +387 -0
  46. quantalogic/tools/finance/google_finance.py +192 -0
  47. quantalogic/tools/finance/market_intelligence_tool.py +520 -0
  48. quantalogic/tools/finance/technical_analysis_tool.py +491 -0
  49. quantalogic/tools/finance/tradingview_tool.py +336 -0
  50. quantalogic/tools/finance/yahoo_finance.py +236 -0
  51. quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
  52. quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
  53. quantalogic/tools/git/clone_repo_tool.py +189 -0
  54. quantalogic/tools/git/git_operations_tool.py +532 -0
  55. quantalogic/tools/google_packages/google_news_tool.py +480 -0
  56. quantalogic/tools/grep_app_tool.py +123 -186
  57. quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
  58. quantalogic/tools/jinja_tool.py +6 -10
  59. quantalogic/tools/language_handlers/__init__.py +22 -9
  60. quantalogic/tools/list_directory_tool.py +131 -42
  61. quantalogic/tools/llm_tool.py +45 -15
  62. quantalogic/tools/llm_vision_tool.py +59 -7
  63. quantalogic/tools/markitdown_tool.py +17 -5
  64. quantalogic/tools/nasa_packages/models.py +47 -0
  65. quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
  66. quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
  67. quantalogic/tools/nasa_packages/services.py +82 -0
  68. quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
  69. quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
  70. quantalogic/tools/product_hunt/services.py +63 -0
  71. quantalogic/tools/rag_tool/__init__.py +48 -0
  72. quantalogic/tools/rag_tool/document_metadata.py +15 -0
  73. quantalogic/tools/rag_tool/query_response.py +20 -0
  74. quantalogic/tools/rag_tool/rag_tool.py +566 -0
  75. quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
  76. quantalogic/tools/read_html_tool.py +24 -38
  77. quantalogic/tools/replace_in_file_tool.py +10 -10
  78. quantalogic/tools/safe_python_interpreter_tool.py +10 -24
  79. quantalogic/tools/search_definition_names.py +2 -2
  80. quantalogic/tools/sequence_tool.py +14 -23
  81. quantalogic/tools/sql_query_tool.py +17 -19
  82. quantalogic/tools/tool.py +39 -15
  83. quantalogic/tools/unified_diff_tool.py +1 -1
  84. quantalogic/tools/utilities/csv_processor_tool.py +234 -0
  85. quantalogic/tools/utilities/download_file_tool.py +179 -0
  86. quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
  87. quantalogic/tools/utils/__init__.py +1 -4
  88. quantalogic/tools/utils/create_sample_database.py +24 -38
  89. quantalogic/tools/utils/generate_database_report.py +74 -82
  90. quantalogic/tools/wikipedia_search_tool.py +17 -21
  91. quantalogic/utils/ask_user_validation.py +1 -1
  92. quantalogic/utils/async_utils.py +35 -0
  93. quantalogic/utils/check_version.py +3 -5
  94. quantalogic/utils/get_all_models.py +2 -1
  95. quantalogic/utils/git_ls.py +21 -7
  96. quantalogic/utils/lm_studio_model_info.py +9 -7
  97. quantalogic/utils/python_interpreter.py +113 -43
  98. quantalogic/utils/xml_utility.py +178 -0
  99. quantalogic/version_check.py +1 -1
  100. quantalogic/welcome_message.py +7 -7
  101. quantalogic/xml_parser.py +0 -1
  102. {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/METADATA +44 -1
  103. quantalogic-0.40.0.dist-info/RECORD +148 -0
  104. quantalogic-0.33.4.dist-info/RECORD +0 -102
  105. {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/LICENSE +0 -0
  106. {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/WHEEL +0 -0
  107. {quantalogic-0.33.4.dist-info → quantalogic-0.40.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
- from typing import Optional
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.generate_with_history(
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
- result = md.convert(file_path)
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 converting file to Markdown: {str(e)}"
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")