signalwire-agents 0.1.10__py3-none-any.whl → 0.1.12__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 (46) hide show
  1. signalwire_agents/__init__.py +43 -4
  2. signalwire_agents/agent_server.py +268 -15
  3. signalwire_agents/cli/__init__.py +9 -0
  4. signalwire_agents/cli/build_search.py +457 -0
  5. signalwire_agents/cli/test_swaig.py +2609 -0
  6. signalwire_agents/core/agent_base.py +691 -82
  7. signalwire_agents/core/contexts.py +289 -0
  8. signalwire_agents/core/data_map.py +499 -0
  9. signalwire_agents/core/function_result.py +57 -10
  10. signalwire_agents/core/logging_config.py +232 -0
  11. signalwire_agents/core/skill_base.py +27 -37
  12. signalwire_agents/core/skill_manager.py +89 -23
  13. signalwire_agents/core/swaig_function.py +13 -1
  14. signalwire_agents/core/swml_handler.py +37 -13
  15. signalwire_agents/core/swml_service.py +37 -28
  16. signalwire_agents/search/__init__.py +131 -0
  17. signalwire_agents/search/document_processor.py +764 -0
  18. signalwire_agents/search/index_builder.py +534 -0
  19. signalwire_agents/search/query_processor.py +371 -0
  20. signalwire_agents/search/search_engine.py +383 -0
  21. signalwire_agents/search/search_service.py +251 -0
  22. signalwire_agents/skills/datasphere/__init__.py +12 -0
  23. signalwire_agents/skills/datasphere/skill.py +229 -0
  24. signalwire_agents/skills/datasphere_serverless/__init__.py +1 -0
  25. signalwire_agents/skills/datasphere_serverless/skill.py +156 -0
  26. signalwire_agents/skills/datetime/skill.py +9 -5
  27. signalwire_agents/skills/joke/__init__.py +1 -0
  28. signalwire_agents/skills/joke/skill.py +88 -0
  29. signalwire_agents/skills/math/skill.py +9 -6
  30. signalwire_agents/skills/native_vector_search/__init__.py +1 -0
  31. signalwire_agents/skills/native_vector_search/skill.py +352 -0
  32. signalwire_agents/skills/registry.py +10 -4
  33. signalwire_agents/skills/web_search/skill.py +57 -21
  34. signalwire_agents/skills/wikipedia/__init__.py +9 -0
  35. signalwire_agents/skills/wikipedia/skill.py +180 -0
  36. signalwire_agents/utils/__init__.py +14 -0
  37. signalwire_agents/utils/schema_utils.py +111 -44
  38. signalwire_agents-0.1.12.dist-info/METADATA +863 -0
  39. signalwire_agents-0.1.12.dist-info/RECORD +67 -0
  40. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/WHEEL +1 -1
  41. signalwire_agents-0.1.12.dist-info/entry_points.txt +3 -0
  42. signalwire_agents-0.1.10.dist-info/METADATA +0 -319
  43. signalwire_agents-0.1.10.dist-info/RECORD +0 -44
  44. {signalwire_agents-0.1.10.data → signalwire_agents-0.1.12.data}/data/schema.json +0 -0
  45. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/licenses/LICENSE +0 -0
  46. {signalwire_agents-0.1.10.dist-info → signalwire_agents-0.1.12.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,180 @@
1
+ """
2
+ Wikipedia Search Skill
3
+
4
+ Provides Wikipedia search capabilities using the Wikipedia API.
5
+ """
6
+
7
+ import requests
8
+ from urllib.parse import quote
9
+ from typing import Dict, Any, Optional
10
+ from signalwire_agents.core.skill_base import SkillBase
11
+
12
+
13
+ class WikipediaSearchSkill(SkillBase):
14
+ """
15
+ Skill for searching Wikipedia articles and retrieving content.
16
+
17
+ This skill uses the Wikipedia API to search for articles and retrieve
18
+ their introductory content, similar to getting a summary of a topic.
19
+ """
20
+
21
+ # Skill metadata
22
+ SKILL_NAME = "wikipedia_search"
23
+ SKILL_DESCRIPTION = "Search Wikipedia for information about a topic and get article summaries"
24
+ SKILL_VERSION = "1.0.0"
25
+ REQUIRED_PACKAGES = ["requests"]
26
+ REQUIRED_ENV_VARS = [] # No environment variables required
27
+
28
+ # Does not support multiple instances
29
+ SUPPORTS_MULTIPLE_INSTANCES = False
30
+
31
+ def setup(self) -> bool:
32
+ """
33
+ Setup the Wikipedia search skill.
34
+
35
+ Returns:
36
+ True if setup successful, False otherwise
37
+ """
38
+ # Extract configuration from params
39
+ self.num_results = max(1, self.params.get('num_results', 1)) # Ensure at least 1 result
40
+ self.no_results_message = self.params.get('no_results_message') or (
41
+ "I couldn't find any Wikipedia articles for '{query}'. "
42
+ "Try rephrasing your search or using different keywords."
43
+ )
44
+
45
+ # Validate that requests package is available
46
+ if not self.validate_packages():
47
+ return False
48
+
49
+ self.logger.info(f"Wikipedia search skill initialized with {self.num_results} max results")
50
+ return True
51
+
52
+ def register_tools(self) -> None:
53
+ """
54
+ Register the SWAIG tool for Wikipedia search.
55
+ """
56
+ self.agent.define_tool(
57
+ name="search_wiki",
58
+ description="Search Wikipedia for information about a topic and get article summaries",
59
+ parameters={
60
+ "query": {
61
+ "type": "string",
62
+ "description": "The search term or topic to look up on Wikipedia"
63
+ }
64
+ },
65
+ handler=self._search_wiki_handler,
66
+ **self.swaig_fields
67
+ )
68
+
69
+ def _search_wiki_handler(self, args, raw_data):
70
+ """Handler for search_wiki tool"""
71
+ from signalwire_agents.core.function_result import SwaigFunctionResult
72
+
73
+ query = args.get("query", "").strip()
74
+ if not query:
75
+ return SwaigFunctionResult("Please provide a search query for Wikipedia.")
76
+
77
+ result = self.search_wiki(query)
78
+ return SwaigFunctionResult(result)
79
+
80
+ def search_wiki(self, query: str) -> str:
81
+ """
82
+ Search Wikipedia for articles matching the query.
83
+
84
+ Args:
85
+ query: The search term to look up
86
+
87
+ Returns:
88
+ String containing the Wikipedia article content or error message
89
+ """
90
+ try:
91
+ # Step 1: Search for articles matching the query
92
+ search_url = (
93
+ "https://en.wikipedia.org/w/api.php"
94
+ "?action=query&list=search&format=json"
95
+ f"&srsearch={quote(query)}"
96
+ f"&srlimit={self.num_results}"
97
+ )
98
+
99
+ response = requests.get(search_url, timeout=10)
100
+ response.raise_for_status()
101
+ search_data = response.json()
102
+
103
+ # Check if we got any search results
104
+ search_results = search_data.get('query', {}).get('search', [])
105
+ if not search_results:
106
+ return self.no_results_message.format(query=query)
107
+
108
+ # Step 2: Get article extracts for each result
109
+ articles = []
110
+ for i, result in enumerate(search_results[:self.num_results]):
111
+ title = result['title']
112
+
113
+ # Get the page extract
114
+ extract_url = (
115
+ "https://en.wikipedia.org/w/api.php"
116
+ "?action=query&prop=extracts&exintro&explaintext&format=json"
117
+ f"&titles={quote(title)}"
118
+ )
119
+
120
+ extract_response = requests.get(extract_url, timeout=10)
121
+ extract_response.raise_for_status()
122
+ extract_data = extract_response.json()
123
+
124
+ # Get the first (and only) page from the response
125
+ pages = extract_data.get('query', {}).get('pages', {})
126
+ if pages:
127
+ page = next(iter(pages.values()))
128
+ extract = page.get('extract', '').strip()
129
+
130
+ if extract:
131
+ # Format the article with title and content
132
+ articles.append(f"**{title}**\n\n{extract}")
133
+ else:
134
+ # Handle case where extract is empty
135
+ articles.append(f"**{title}**\n\nNo summary available for this article.")
136
+
137
+ if not articles:
138
+ return self.no_results_message.format(query=query)
139
+
140
+ # Join multiple articles with separators
141
+ if len(articles) == 1:
142
+ return articles[0]
143
+ else:
144
+ return "\n\n" + "="*50 + "\n\n".join(articles)
145
+
146
+ except requests.exceptions.RequestException as e:
147
+ return f"Error accessing Wikipedia: {str(e)}"
148
+ except Exception as e:
149
+ return f"Error searching Wikipedia: {str(e)}"
150
+
151
+ def get_prompt_sections(self) -> list:
152
+ """
153
+ Return additional context for the agent prompt.
154
+
155
+ Returns:
156
+ List of prompt sections to add to the agent
157
+ """
158
+ return [{
159
+ "title": "Wikipedia Search",
160
+ "body": f"You can search Wikipedia for factual information using search_wiki. This will return up to {self.num_results} Wikipedia article summaries.",
161
+ "bullets": [
162
+ "Use search_wiki for factual, encyclopedic information",
163
+ "Great for answering questions about people, places, concepts, and history",
164
+ "Returns reliable, well-sourced information from Wikipedia articles"
165
+ ]
166
+ }]
167
+
168
+ def get_hints(self) -> list:
169
+ """
170
+ Return speech recognition hints for better accuracy.
171
+
172
+ Returns:
173
+ List of words/phrases to help with speech recognition
174
+ """
175
+ # Currently no hints provided, but you could add them like:
176
+ # return [
177
+ # "Wikipedia", "wiki", "search Wikipedia", "look up", "tell me about",
178
+ # "what is", "who is", "information about", "facts about"
179
+ # ]
180
+ return []
@@ -7,3 +7,17 @@ Licensed under the MIT License.
7
7
  See LICENSE file in the project root for full license information.
8
8
  """
9
9
 
10
+ from .schema_utils import SchemaUtils
11
+ from signalwire_agents.core.logging_config import get_execution_mode
12
+
13
+ def is_serverless_mode() -> bool:
14
+ """
15
+ Check if running in any serverless environment.
16
+
17
+ Returns:
18
+ bool: True if in serverless mode, False if in server mode
19
+ """
20
+ return get_execution_mode() != 'server'
21
+
22
+ __all__ = ["SchemaUtils", "get_execution_mode", "is_serverless_mode"]
23
+
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env python3
1
2
  """
2
3
  Copyright (c) 2025 SignalWire
3
4
 
@@ -7,62 +8,129 @@ Licensed under the MIT License.
7
8
  See LICENSE file in the project root for full license information.
8
9
  """
9
10
 
11
+ # -*- coding: utf-8 -*-
10
12
  """
11
- Utilities for working with the SWML JSON schema.
12
-
13
- This module provides functions for loading, parsing, and validating SWML schemas.
14
- It also provides utilities for working with SWML documents based on the schema.
13
+ Schema utilities for SWML validation and verb extraction
15
14
  """
16
15
 
17
- import json
18
16
  import os
19
- import re
20
- from typing import Dict, List, Any, Optional, Set, Tuple, Callable
17
+ import json
18
+ import logging
19
+ from typing import Dict, Any, List, Optional, Tuple
21
20
 
21
+ try:
22
+ import structlog
23
+ # Ensure structlog is configured
24
+ if not structlog.is_configured():
25
+ structlog.configure(
26
+ processors=[
27
+ structlog.stdlib.filter_by_level,
28
+ structlog.stdlib.add_logger_name,
29
+ structlog.stdlib.add_log_level,
30
+ structlog.stdlib.PositionalArgumentsFormatter(),
31
+ structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
32
+ structlog.processors.StackInfoRenderer(),
33
+ structlog.processors.format_exc_info,
34
+ structlog.processors.UnicodeDecoder(),
35
+ structlog.dev.ConsoleRenderer()
36
+ ],
37
+ context_class=dict,
38
+ logger_factory=structlog.stdlib.LoggerFactory(),
39
+ wrapper_class=structlog.stdlib.BoundLogger,
40
+ cache_logger_on_first_use=True,
41
+ )
42
+ except ImportError:
43
+ raise ImportError(
44
+ "structlog is required. Install it with: pip install structlog"
45
+ )
46
+
47
+ # Create a logger
48
+ logger = structlog.get_logger("schema_utils")
22
49
 
23
50
  class SchemaUtils:
24
51
  """
25
- Utilities for working with SWML JSON schema.
26
-
27
- This class provides methods for:
28
- - Loading and parsing schema files
29
- - Extracting verb definitions
30
- - Validating SWML objects against the schema
31
- - Generating helpers for schema operations
52
+ Utility class for loading and working with SWML schemas
32
53
  """
33
54
 
34
55
  def __init__(self, schema_path: Optional[str] = None):
35
56
  """
36
- Initialize the SchemaUtils with the provided schema
57
+ Initialize the schema utilities
37
58
 
38
59
  Args:
39
- schema_path: Path to the schema file (optional)
60
+ schema_path: Path to the schema file
40
61
  """
62
+ self.log = logger.bind(component="schema_utils")
63
+
41
64
  self.schema_path = schema_path
42
65
  if not self.schema_path:
43
66
  self.schema_path = self._get_default_schema_path()
44
- print(f"No schema_path provided, using default: {self.schema_path}")
67
+ self.log.debug("using_default_schema_path", path=self.schema_path)
45
68
 
46
69
  self.schema = self.load_schema()
47
70
  self.verbs = self._extract_verb_definitions()
48
- print(f"Extracted {len(self.verbs)} verbs from schema")
71
+ self.log.debug("schema_initialized", verb_count=len(self.verbs))
49
72
  if self.verbs:
50
- print(f"First few verbs: {list(self.verbs.keys())[:5]}")
73
+ self.log.debug("first_verbs_extracted", verbs=list(self.verbs.keys())[:5])
51
74
 
52
- def _get_default_schema_path(self) -> str:
75
+ def _get_default_schema_path(self) -> Optional[str]:
53
76
  """
54
- Get the default path to the schema file
77
+ Get the default path to the schema file using the same robust logic as SWMLService
55
78
 
56
79
  Returns:
57
- Path to the schema file
80
+ Path to the schema file or None if not found
58
81
  """
59
- # Default path is the schema.json in the root directory
60
- package_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
61
- default_path = os.path.join(package_dir, "schema.json")
62
- print(f"Default schema path: {default_path}")
63
- print(f"Path exists: {os.path.exists(default_path)}")
64
-
65
- return default_path
82
+ # Try package resources first (most reliable after pip install)
83
+ try:
84
+ import importlib.resources
85
+ try:
86
+ # Python 3.9+
87
+ try:
88
+ # Python 3.13+
89
+ path = importlib.resources.files("signalwire_agents").joinpath("schema.json")
90
+ return str(path)
91
+ except Exception:
92
+ # Python 3.9-3.12
93
+ with importlib.resources.files("signalwire_agents").joinpath("schema.json") as path:
94
+ return str(path)
95
+ except AttributeError:
96
+ # Python 3.7-3.8
97
+ with importlib.resources.path("signalwire_agents", "schema.json") as path:
98
+ return str(path)
99
+ except (ImportError, ModuleNotFoundError):
100
+ pass
101
+
102
+ # Fall back to pkg_resources for older Python or alternative lookup
103
+ try:
104
+ import pkg_resources
105
+ return pkg_resources.resource_filename("signalwire_agents", "schema.json")
106
+ except (ImportError, ModuleNotFoundError, pkg_resources.DistributionNotFound):
107
+ pass
108
+
109
+ # Fall back to manual search in various locations
110
+ import sys
111
+
112
+ # Get package directory relative to this file
113
+ package_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
114
+
115
+ # Potential locations for schema.json
116
+ potential_paths = [
117
+ os.path.join(os.getcwd(), "schema.json"), # Current working directory
118
+ os.path.join(package_dir, "schema.json"), # Package directory
119
+ os.path.join(os.path.dirname(package_dir), "schema.json"), # Parent of package directory
120
+ os.path.join(sys.prefix, "schema.json"), # Python installation directory
121
+ os.path.join(package_dir, "data", "schema.json"), # Data subdirectory
122
+ os.path.join(os.path.dirname(package_dir), "data", "schema.json"), # Parent's data subdirectory
123
+ ]
124
+
125
+ # Try to find the schema file
126
+ for path in potential_paths:
127
+ self.log.debug("checking_schema_path", path=path, exists=os.path.exists(path))
128
+ if os.path.exists(path):
129
+ self.log.debug("schema_found_at", path=path)
130
+ return path
131
+
132
+ self.log.warning("schema_not_found_in_any_location")
133
+ return None
66
134
 
67
135
  def load_schema(self) -> Dict[str, Any]:
68
136
  """
@@ -72,27 +140,26 @@ class SchemaUtils:
72
140
  The schema as a dictionary
73
141
  """
74
142
  if not self.schema_path:
75
- print(f"Warning: No schema path provided. Using empty schema.")
143
+ self.log.warning("no_schema_path_provided")
76
144
  return {}
77
145
 
78
146
  try:
79
- print(f"Attempting to load schema from: {self.schema_path}")
80
- print(f"File exists: {os.path.exists(self.schema_path)}")
147
+ self.log.debug("loading_schema", path=self.schema_path, exists=os.path.exists(self.schema_path))
81
148
 
82
149
  if os.path.exists(self.schema_path):
83
150
  with open(self.schema_path, "r") as f:
84
151
  schema = json.load(f)
85
- print(f"Successfully loaded schema from {self.schema_path}")
86
- print(f"Schema has {len(schema.keys()) if schema else 0} top-level keys")
152
+ self.log.debug("schema_loaded_successfully",
153
+ path=self.schema_path,
154
+ top_level_keys=len(schema.keys()) if schema else 0)
87
155
  if "$defs" in schema:
88
- print(f"Schema has {len(schema['$defs'])} definitions")
156
+ self.log.debug("schema_definitions_found", count=len(schema['$defs']))
89
157
  return schema
90
158
  else:
91
- print(f"Schema file not found at {self.schema_path}")
159
+ self.log.error("schema_file_not_found", path=self.schema_path)
92
160
  return {}
93
161
  except (FileNotFoundError, json.JSONDecodeError) as e:
94
- print(f"Error loading schema: {e}")
95
- print(f"Using empty schema as fallback")
162
+ self.log.error("schema_loading_error", error=str(e), path=self.schema_path)
96
163
  return {}
97
164
 
98
165
  def _extract_verb_definitions(self) -> Dict[str, Dict[str, Any]]:
@@ -107,17 +174,17 @@ class SchemaUtils:
107
174
  # Extract from SWMLMethod anyOf
108
175
  if "$defs" in self.schema and "SWMLMethod" in self.schema["$defs"]:
109
176
  swml_method = self.schema["$defs"]["SWMLMethod"]
110
- print(f"Found SWMLMethod in schema with keys: {swml_method.keys()}")
177
+ self.log.debug("swml_method_found", keys=list(swml_method.keys()))
111
178
 
112
179
  if "anyOf" in swml_method:
113
- print(f"Found anyOf in SWMLMethod with {len(swml_method['anyOf'])} items")
180
+ self.log.debug("anyof_found", count=len(swml_method['anyOf']))
114
181
 
115
182
  for ref in swml_method["anyOf"]:
116
183
  if "$ref" in ref:
117
184
  # Extract the verb name from the reference
118
185
  verb_ref = ref["$ref"]
119
186
  verb_name = verb_ref.split("/")[-1]
120
- print(f"Processing verb reference: {verb_ref} -> {verb_name}")
187
+ self.log.debug("processing_verb_reference", ref=verb_ref, name=verb_name)
121
188
 
122
189
  # Look up the verb definition
123
190
  if verb_name in self.schema["$defs"]:
@@ -133,11 +200,11 @@ class SchemaUtils:
133
200
  "schema_name": verb_name,
134
201
  "definition": verb_def
135
202
  }
136
- print(f"Added verb: {actual_verb}")
203
+ self.log.debug("verb_added", verb=actual_verb)
137
204
  else:
138
- print(f"Missing $defs or SWMLMethod in schema")
205
+ self.log.warning("missing_swml_method_or_defs")
139
206
  if "$defs" in self.schema:
140
- print(f"Available definitions: {list(self.schema['$defs'].keys())}")
207
+ self.log.debug("available_definitions", defs=list(self.schema['$defs'].keys()))
141
208
 
142
209
  return verbs
143
210