semantio 0.0.1__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.
@@ -0,0 +1,128 @@
1
+ # duckduckgo.py
2
+ from .base_tool import BaseTool
3
+ from duckduckgo_search import DDGS
4
+ from typing import Dict, Any, List, Optional
5
+ from pydantic import Field
6
+
7
+ class DuckDuckGo(BaseTool):
8
+ enable_web_search: bool = Field(default=True, description="Enable web search functionality.")
9
+ enable_news_search: bool = Field(default=False, description="Enable news search functionality.")
10
+ enable_image_search: bool = Field(default=False, description="Enable image search functionality.")
11
+ enable_video_search: bool = Field(default=False, description="Enable video search functionality.")
12
+ region: Optional[str] = Field(default="wt-wt", description="Region for search results (e.g., 'us-en', 'fr-fr').")
13
+ safesearch: str = Field(default="moderate", description="Safesearch filter ('on', 'moderate', 'off').")
14
+ time_range: Optional[str] = Field(default=None, description="Time range for search results ('d', 'w', 'm', 'y').")
15
+
16
+ def __init__(self, **kwargs):
17
+ super().__init__(name="DuckDuckGo", description="Perform web, news, image, and video searches using DuckDuckGo.", **kwargs)
18
+
19
+ def execute(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
20
+ """
21
+ Execute a search using DuckDuckGo based on the provided input.
22
+
23
+ Args:
24
+ input_data (Dict[str, Any]): Input data containing the query and optional parameters.
25
+
26
+ Returns:
27
+ Dict[str, Any]: Search results for the requested types.
28
+ """
29
+ query = input_data.get("query", "")
30
+ max_results = input_data.get("max_results", 5) # Default to 5 results
31
+ region = input_data.get("region", self.region)
32
+ safesearch = input_data.get("safesearch", self.safesearch)
33
+ time_range = input_data.get("time_range", self.time_range)
34
+
35
+ results = {}
36
+
37
+ # Perform web search
38
+ if self.enable_web_search:
39
+ results["web"] = self._perform_search(
40
+ query, max_results, region, safesearch, time_range, search_type="text"
41
+ )
42
+
43
+ # Perform news search
44
+ if self.enable_news_search:
45
+ results["news"] = self._perform_search(
46
+ query, max_results, region, safesearch, time_range, search_type="news"
47
+ )
48
+
49
+ # Perform image search
50
+ if self.enable_image_search:
51
+ results["images"] = self._perform_search(
52
+ query, max_results, region, safesearch, time_range, search_type="images"
53
+ )
54
+
55
+ # Perform video search
56
+ if self.enable_video_search:
57
+ results["videos"] = self._perform_search(
58
+ query, max_results, region, safesearch, time_range, search_type="videos"
59
+ )
60
+
61
+ return results
62
+
63
+ def _perform_search(
64
+ self,
65
+ query: str,
66
+ max_results: int,
67
+ region: str,
68
+ safesearch: str,
69
+ time_range: Optional[str],
70
+ search_type: str,
71
+ ) -> List[Dict[str, Any]]:
72
+ """
73
+ Perform a search using DuckDuckGo.
74
+
75
+ Args:
76
+ query (str): The search query.
77
+ max_results (int): Maximum number of results to return.
78
+ region (str): Region for search results.
79
+ safesearch (str): Safesearch filter.
80
+ time_range (Optional[str]): Time range for search results.
81
+ search_type (str): Type of search ("text", "news", "images", "videos").
82
+
83
+ Returns:
84
+ List[Dict[str, Any]]: List of search results.
85
+ """
86
+ with DDGS() as ddgs:
87
+ if search_type == "text":
88
+ return list(
89
+ ddgs.text(
90
+ keywords=query,
91
+ max_results=max_results,
92
+ region=region,
93
+ safesearch=safesearch,
94
+ timelimit=time_range,
95
+ )
96
+ )
97
+ elif search_type == "news":
98
+ return list(
99
+ ddgs.news(
100
+ keywords=query,
101
+ max_results=max_results,
102
+ region=region,
103
+ safesearch=safesearch,
104
+ timelimit=time_range,
105
+ )
106
+ )
107
+ elif search_type == "images":
108
+ return list(
109
+ ddgs.images(
110
+ keywords=query,
111
+ max_results=max_results,
112
+ region=region,
113
+ safesearch=safesearch,
114
+ timelimit=time_range,
115
+ )
116
+ )
117
+ elif search_type == "videos":
118
+ return list(
119
+ ddgs.videos(
120
+ keywords=query,
121
+ max_results=max_results,
122
+ region=region,
123
+ safesearch=safesearch,
124
+ timelimit=time_range,
125
+ )
126
+ )
127
+ else:
128
+ raise ValueError(f"Unsupported search type: {search_type}")
@@ -0,0 +1,131 @@
1
+ from .base_tool import BaseTool
2
+ import yfinance as yf
3
+ from typing import Dict, Any, List, Optional
4
+ from pydantic import Field
5
+ import logging
6
+ import re
7
+
8
+ # Configure logging
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class StockPriceChecker(BaseTool):
13
+ default_symbol: str = Field(default="AAPL", description="Default stock symbol (e.g., 'AAPL').")
14
+ enable_historical_data: bool = Field(default=True, description="Enable fetching historical data.")
15
+ enable_multiple_symbols: bool = Field(default=True, description="Enable fetching data for multiple stocks.")
16
+ enable_metrics: bool = Field(default=True, description="Enable additional metrics like volume and market cap.")
17
+ llm: Optional[Any] = Field(None, description="The LLM instance to use for symbol extraction.")
18
+
19
+ def __init__(self, **kwargs):
20
+ super().__init__(
21
+ name="StockPriceChecker",
22
+ description="Fetch real-time and historical stock prices and metrics.",
23
+ **kwargs
24
+ )
25
+
26
+ def execute(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
27
+ """
28
+ Execute the stock price check based on the provided input.
29
+
30
+ Args:
31
+ input_data (Dict[str, Any]): Input data containing the query and optional parameters.
32
+
33
+ Returns:
34
+ Dict[str, Any]: Stock price and additional metrics.
35
+ """
36
+ try:
37
+ query = input_data.get("query", "")
38
+ symbols = self._extract_symbols(query) if query else [self.default_symbol]
39
+ time_range = input_data.get("time_range", "1d") # Default to 1 day
40
+ metrics = input_data.get("metrics", ["price"]) # Default to price only
41
+
42
+ results = {}
43
+
44
+ for symbol in symbols:
45
+ # Validate the symbol
46
+ if not self._is_valid_symbol(symbol):
47
+ logger.warning(f"Invalid stock symbol: {symbol}")
48
+ results[symbol] = {"error": "Invalid symbol"}
49
+ continue
50
+
51
+ try:
52
+ stock = yf.Ticker(symbol)
53
+ data = {}
54
+
55
+ # Fetch real-time price
56
+ if "price" in metrics:
57
+ history = stock.history(period=time_range)
58
+ if not history.empty:
59
+ data["price"] = history["Close"].iloc[-1]
60
+ else:
61
+ data["price"] = "No data available"
62
+
63
+ # Fetch historical data
64
+ if self.enable_historical_data and "history" in metrics:
65
+ history = stock.history(period=time_range)
66
+ if not history.empty:
67
+ data["history"] = history.to_dict()
68
+ else:
69
+ data["history"] = "No historical data available"
70
+
71
+ # Fetch additional metrics
72
+ if self.enable_metrics:
73
+ info = stock.info
74
+ if "volume" in metrics:
75
+ data["volume"] = info.get("volume", "No volume data available")
76
+ if "market_cap" in metrics:
77
+ data["market_cap"] = info.get("marketCap", "No market cap data available")
78
+
79
+ results[symbol] = data
80
+ except Exception as e:
81
+ results[symbol] = {"error": str(e)}
82
+
83
+ return results
84
+ except Exception as e:
85
+ return {"error": str(e)}
86
+
87
+ def _extract_symbols(self, query: str) -> List[str]:
88
+ """
89
+ Use the LLM to extract stock symbols from the user's query.
90
+ """
91
+ if not self.llm:
92
+ logger.error("LLM instance not available for symbol extraction.")
93
+ return []
94
+
95
+ # Create a prompt for the LLM
96
+ prompt = f"""
97
+ Extract the stock symbols from the following user query.
98
+ Return ONLY the symbols as a comma-separated list (e.g., "AAPL,MSFT").
99
+ Do not include any explanations or additional text.
100
+ If no symbols are found, return "None".
101
+
102
+ User Query: "{query}"
103
+ """
104
+
105
+ try:
106
+ # Call the LLM to generate the response
107
+ response = self.llm.generate(prompt=prompt)
108
+ logger.info(f"LLM response: {response}")
109
+ symbols = response.strip().replace('"', '').replace("'", "")
110
+
111
+ # Fallback: Extract symbols from verbose responses
112
+ if ":" in symbols or "\n" in symbols:
113
+ # Look for patterns like "AAPL,MSFT" in the response
114
+ symbol_pattern = re.compile(r"\b[A-Z]{1,5}\b")
115
+ symbols = ",".join(symbol_pattern.findall(symbols))
116
+ logger.info(f"Extracted symbols: {symbols}")
117
+
118
+ # Parse the response into a list of symbols
119
+ if symbols.lower() == "none":
120
+ return []
121
+ return [s.strip() for s in symbols.split(",")]
122
+ except Exception as e:
123
+ logger.error(f"Failed to extract symbols: {e}")
124
+ return []
125
+
126
+ def _is_valid_symbol(self, symbol: str) -> bool:
127
+ """
128
+ Validate if the symbol is a valid stock symbol.
129
+ """
130
+ # Check if the symbol is a valid stock ticker (e.g., AAPL, MSFT)
131
+ return bool(symbol.strip()) and symbol.isalpha() and 1 <= len(symbol) <= 5
@@ -0,0 +1,153 @@
1
+ from typing import Dict, Any, Optional, List
2
+ from playwright.async_api import async_playwright
3
+ import asyncio
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class WebBrowserTool:
9
+ """
10
+ A tool for performing browser automation tasks using Playwright.
11
+ """
12
+
13
+ def __init__(self, headless: bool = True):
14
+ """
15
+ Initialize the WebBrowserTool.
16
+
17
+ Args:
18
+ headless (bool): Whether to run the browser in headless mode (default: True).
19
+ """
20
+ self.headless = headless
21
+ self.browser = None
22
+ self.context = None
23
+ self.page = None
24
+
25
+ async def start(self):
26
+ """
27
+ Start the browser and create a new context and page.
28
+ """
29
+ self.playwright = await async_playwright().start()
30
+ self.browser = await self.playwright.chromium.launch(headless=self.headless)
31
+ self.context = await self.browser.new_context()
32
+ self.page = await self.context.new_page()
33
+ logger.info("Browser started successfully.")
34
+
35
+ async def close(self):
36
+ """
37
+ Close the browser and cleanup resources.
38
+ """
39
+ if self.browser:
40
+ await self.browser.close()
41
+ await self.playwright.stop()
42
+ logger.info("Browser closed successfully.")
43
+
44
+ async def navigate(self, url: str) -> str:
45
+ """
46
+ Navigate to a specific URL.
47
+
48
+ Args:
49
+ url (str): The URL to navigate to.
50
+
51
+ Returns:
52
+ str: The page title after navigation.
53
+ """
54
+ if not self.page:
55
+ raise RuntimeError("Browser is not started. Call start() first.")
56
+
57
+ await self.page.goto(url)
58
+ title = await self.page.title()
59
+ logger.info(f"Navigated to {url}. Page title: {title}")
60
+ return title
61
+
62
+ async def fill_form(self, fields: Dict[str, str]) -> str:
63
+ """
64
+ Fill a form with the provided fields.
65
+
66
+ Args:
67
+ fields (Dict[str, str]): A dictionary of field names and values to fill.
68
+
69
+ Returns:
70
+ str: A success message.
71
+ """
72
+ if not self.page:
73
+ raise RuntimeError("Browser is not started. Call start() first.")
74
+
75
+ for field, value in fields.items():
76
+ await self.page.fill(f'input[name="{field}"]', value)
77
+ logger.info(f"Filled field '{field}' with value '{value}'.")
78
+
79
+ return "Form filled successfully."
80
+
81
+ async def click(self, selector: str) -> str:
82
+ """
83
+ Click an element on the page.
84
+
85
+ Args:
86
+ selector (str): The CSS selector of the element to click.
87
+
88
+ Returns:
89
+ str: A success message.
90
+ """
91
+ if not self.page:
92
+ raise RuntimeError("Browser is not started. Call start() first.")
93
+
94
+ await self.page.click(selector)
95
+ logger.info(f"Clicked element with selector '{selector}'.")
96
+ return f"Clicked element: {selector}"
97
+
98
+ async def scrape(self, selector: str) -> List[Dict[str, str]]:
99
+ """
100
+ Scrape data from the page.
101
+
102
+ Args:
103
+ selector (str): The CSS selector of the elements to scrape.
104
+
105
+ Returns:
106
+ List[Dict[str, str]]: A list of dictionaries containing the scraped data.
107
+ """
108
+ if not self.page:
109
+ raise RuntimeError("Browser is not started. Call start() first.")
110
+
111
+ elements = await self.page.query_selector_all(selector)
112
+ scraped_data = []
113
+ for element in elements:
114
+ text = await element.inner_text()
115
+ scraped_data.append({"text": text.strip()})
116
+ logger.info(f"Scraped text: {text.strip()}")
117
+
118
+ return scraped_data
119
+
120
+ async def execute_step(self, step: Dict[str, Any]) -> str:
121
+ """
122
+ Execute a browser automation step.
123
+
124
+ Args:
125
+ step (Dict[str, Any]): A dictionary containing the step details.
126
+ - "action": The action to perform (e.g., "navigate", "fill_form", "click", "scrape").
127
+ - "details": The details required for the action (e.g., URL, form fields, selector).
128
+ - "website": The website to perform the action on (optional).
129
+
130
+ Returns:
131
+ str: The result of the step execution.
132
+ """
133
+ action = step.get("action")
134
+ details = step.get("details")
135
+ website = step.get("website", "https://www.google.com")
136
+
137
+ if not self.page:
138
+ await self.start()
139
+
140
+ try:
141
+ if action == "navigate":
142
+ return await self.navigate(details)
143
+ elif action == "fill_form":
144
+ return await self.fill_form(details)
145
+ elif action == "click":
146
+ return await self.click(details)
147
+ elif action == "scrape":
148
+ return str(await self.scrape(details))
149
+ else:
150
+ return f"Unknown action: {action}"
151
+ except Exception as e:
152
+ logger.error(f"Error executing step: {e}")
153
+ return f"Error executing step: {e}"
@@ -0,0 +1,7 @@
1
+ from .logger import logger
2
+ from .config import Config
3
+ from .file_utils import FileUtils
4
+ from .date_utils import DateUtils
5
+ from .validation_utils import ValidationUtils
6
+
7
+ __all__ = ["logger", "Config", "FileUtils", "DateUtils", "ValidationUtils"]
@@ -0,0 +1,41 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from pathlib import Path
4
+ from typing import Any, Optional, Type, TypeVar, List, Dict
5
+
6
+ # Load environment variables from .env file
7
+ load_dotenv()
8
+
9
+ class Config:
10
+ """
11
+ Configuration class to load and manage environment variables.
12
+ """
13
+
14
+ @staticmethod
15
+ def get(key: str, default: Optional[str] = None) -> str:
16
+ """
17
+ Get an environment variable. Raise an error if it's not found and no default is provided.
18
+ """
19
+ value = os.getenv(key, default)
20
+ if value is None:
21
+ raise ValueError(f"Environment variable {key} is not set.")
22
+ return value
23
+
24
+ @staticmethod
25
+ def get_bool(key: str, default: bool = False) -> bool:
26
+ """
27
+ Get an environment variable as a boolean.
28
+ """
29
+ value = os.getenv(key, str(default)).lower()
30
+ return value in ("true", "1", "yes")
31
+
32
+ @staticmethod
33
+ def get_int(key: str, default: int = 0) -> int:
34
+ """
35
+ Get an environment variable as an integer.
36
+ """
37
+ return int(os.getenv(key, str(default)))
38
+
39
+ # Example usage:
40
+ # GROQ_API_KEY = Config.get("GROQ_API_KEY")
41
+ # MAX_RETRIES = Config.get_int("MAX_RETRIES", 5)
@@ -0,0 +1,44 @@
1
+ from datetime import datetime, timedelta
2
+ from typing import Optional
3
+
4
+ class DateUtils:
5
+ @staticmethod
6
+ def get_current_time() -> str:
7
+ """
8
+ Get the current time in ISO format.
9
+ """
10
+ return datetime.now().isoformat()
11
+
12
+ @staticmethod
13
+ def format_time(timestamp: str, format: str = "%Y-%m-%d %H:%M:%S") -> str:
14
+ """
15
+ Format a timestamp string into a custom format.
16
+ """
17
+ try:
18
+ dt = datetime.fromisoformat(timestamp)
19
+ return dt.strftime(format)
20
+ except Exception as e:
21
+ raise ValueError(f"Failed to format timestamp {timestamp}: {e}")
22
+
23
+ @staticmethod
24
+ def add_time(timestamp: str, days: int = 0, hours: int = 0, minutes: int = 0) -> str:
25
+ """
26
+ Add days, hours, or minutes to a timestamp.
27
+ """
28
+ try:
29
+ dt = datetime.fromisoformat(timestamp)
30
+ dt += timedelta(days=days, hours=hours, minutes=minutes)
31
+ return dt.isoformat()
32
+ except Exception as e:
33
+ raise ValueError(f"Failed to add time to timestamp {timestamp}: {e}")
34
+
35
+ @staticmethod
36
+ def is_future_time(timestamp: str) -> bool:
37
+ """
38
+ Check if a timestamp is in the future.
39
+ """
40
+ try:
41
+ dt = datetime.fromisoformat(timestamp)
42
+ return dt > datetime.now()
43
+ except Exception as e:
44
+ raise ValueError(f"Failed to check if timestamp {timestamp} is in the future: {e}")
@@ -0,0 +1,56 @@
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional
5
+
6
+ class FileUtils:
7
+ @staticmethod
8
+ def read_file(file_path: str) -> str:
9
+ """
10
+ Read the contents of a file.
11
+ """
12
+ try:
13
+ with open(file_path, "r", encoding="utf-8") as file:
14
+ return file.read()
15
+ except Exception as e:
16
+ raise ValueError(f"Failed to read file {file_path}: {e}")
17
+
18
+ @staticmethod
19
+ def write_file(file_path: str, content: str) -> None:
20
+ """
21
+ Write content to a file.
22
+ """
23
+ try:
24
+ with open(file_path, "w", encoding="utf-8") as file:
25
+ file.write(content)
26
+ except Exception as e:
27
+ raise ValueError(f"Failed to write to file {file_path}: {e}")
28
+
29
+ @staticmethod
30
+ def read_json(file_path: str) -> Dict[str, Any]:
31
+ """
32
+ Read a JSON file and return its contents as a dictionary.
33
+ """
34
+ try:
35
+ with open(file_path, "r", encoding="utf-8") as file:
36
+ return json.load(file)
37
+ except Exception as e:
38
+ raise ValueError(f"Failed to read JSON file {file_path}: {e}")
39
+
40
+ @staticmethod
41
+ def write_json(file_path: str, data: Dict[str, Any]) -> None:
42
+ """
43
+ Write a dictionary to a JSON file.
44
+ """
45
+ try:
46
+ with open(file_path, "w", encoding="utf-8") as file:
47
+ json.dump(data, file, indent=2)
48
+ except Exception as e:
49
+ raise ValueError(f"Failed to write JSON file {file_path}: {e}")
50
+
51
+ @staticmethod
52
+ def ensure_directory_exists(directory: str) -> None:
53
+ """
54
+ Ensure that a directory exists. If not, create it.
55
+ """
56
+ Path(directory).mkdir(parents=True, exist_ok=True)
@@ -0,0 +1,20 @@
1
+ import logging
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ # Create a logs directory if it doesn't exist
6
+ logs_dir = Path("logs")
7
+ logs_dir.mkdir(exist_ok=True)
8
+
9
+ # Logger configuration
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
13
+ handlers=[
14
+ logging.FileHandler(logs_dir / "agent.log"), # Log to a file
15
+ logging.StreamHandler(sys.stdout), # Log to the console
16
+ ],
17
+ )
18
+
19
+ # Create a logger instance
20
+ logger = logging.getLogger("agent")
@@ -0,0 +1,44 @@
1
+ import re
2
+ from typing import Any, Optional
3
+
4
+ class ValidationUtils:
5
+ @staticmethod
6
+ def validate_email(email: str) -> bool:
7
+ """
8
+ Validate an email address.
9
+ """
10
+ regex = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
11
+ return re.match(regex, email) is not None
12
+
13
+ @staticmethod
14
+ def validate_phone_number(phone: str) -> bool:
15
+ """
16
+ Validate a phone number (basic validation for US numbers).
17
+ """
18
+ regex = r"^\+?1?\d{10}$"
19
+ return re.match(regex, phone) is not None
20
+
21
+ @staticmethod
22
+ def validate_not_empty(value: Any) -> bool:
23
+ """
24
+ Validate that a value is not empty.
25
+ """
26
+ if isinstance(value, str):
27
+ return bool(value.strip())
28
+ elif isinstance(value, (list, dict, set)):
29
+ return bool(value)
30
+ return value is not None
31
+
32
+ @staticmethod
33
+ def validate_numeric(value: Any) -> bool:
34
+ """
35
+ Validate that a value is numeric (int or float).
36
+ """
37
+ return isinstance(value, (int, float))
38
+
39
+ @staticmethod
40
+ def validate_in_range(value: float, min: float, max: float) -> bool:
41
+ """
42
+ Validate that a numeric value is within a specified range.
43
+ """
44
+ return min <= value <= max
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Syenah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.