signalwire-agents 0.1.50__py3-none-any.whl → 0.1.53__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.
@@ -18,7 +18,7 @@ A package for building AI agents using SignalWire's AI and SWML capabilities.
18
18
  from .core.logging_config import configure_logging
19
19
  configure_logging()
20
20
 
21
- __version__ = "0.1.50"
21
+ __version__ = "0.1.53"
22
22
 
23
23
  # Import core classes for easier access
24
24
  from .core.agent_base import AgentBase
@@ -69,6 +69,16 @@ Examples:
69
69
  sw-search ./docs \\
70
70
  --chunking-strategy qa
71
71
 
72
+ # Markdown-aware chunking (preserves headers, detects code blocks, adds tags)
73
+ sw-search ./docs \\
74
+ --chunking-strategy markdown \\
75
+ --file-types md
76
+ # This strategy:
77
+ # - Chunks at header boundaries (h1, h2, h3...)
78
+ # - Detects code blocks and extracts language (python, bash, etc)
79
+ # - Adds "code" tags to chunks with code for better search
80
+ # - Preserves section hierarchy in metadata
81
+
72
82
  # Model selection examples (performance vs quality tradeoff)
73
83
  sw-search ./docs --model mini # Fastest (~5x faster), 384 dims, good for most use cases
74
84
  sw-search ./docs --model base # Balanced speed/quality, 768 dims (previous default)
@@ -128,16 +138,23 @@ Examples:
128
138
  --collection-name docs_collection
129
139
  sw-search migrate --info ./docs.swsearch
130
140
 
131
- # PostgreSQL pgvector backend
141
+ # PostgreSQL pgvector backend (direct build to PostgreSQL)
132
142
  sw-search ./docs \\
133
143
  --backend pgvector \\
134
- --connection-string "postgresql://user:pass@localhost/knowledge" \\
144
+ --connection-string "postgresql://user:pass@localhost:5432/knowledge" \\
135
145
  --output docs_collection
136
146
 
147
+ # pgvector with markdown strategy (best for documentation with code examples)
148
+ sw-search ./docs \\
149
+ --backend pgvector \\
150
+ --connection-string "postgresql://user:pass@localhost:5432/knowledge" \\
151
+ --output docs_collection \\
152
+ --chunking-strategy markdown
153
+
137
154
  # Overwrite existing pgvector collection
138
155
  sw-search ./docs \\
139
156
  --backend pgvector \\
140
- --connection-string "postgresql://user:pass@localhost/knowledge" \\
157
+ --connection-string "postgresql://user:pass@localhost:5432/knowledge" \\
141
158
  --output docs_collection \\
142
159
  --overwrite
143
160
 
@@ -191,9 +208,9 @@ Examples:
191
208
 
192
209
  parser.add_argument(
193
210
  '--chunking-strategy',
194
- choices=['sentence', 'sliding', 'paragraph', 'page', 'semantic', 'topic', 'qa', 'json'],
211
+ choices=['sentence', 'sliding', 'paragraph', 'page', 'semantic', 'topic', 'qa', 'json', 'markdown'],
195
212
  default='sentence',
196
- help='Chunking strategy to use (default: sentence)'
213
+ help='Chunking strategy to use (default: sentence). Use "markdown" for documentation with code blocks.'
197
214
  )
198
215
 
199
216
  parser.add_argument(
@@ -53,6 +53,26 @@ class SkillBase(ABC):
53
53
  """Register SWAIG tools with the agent"""
54
54
  pass
55
55
 
56
+ def define_tool(self, **kwargs) -> None:
57
+ """
58
+ Wrapper method that automatically includes swaig_fields when defining tools.
59
+
60
+ This method delegates to self.agent.define_tool() but automatically merges
61
+ any swaig_fields configured for this skill. Skills should use this method
62
+ instead of calling self.agent.define_tool() directly.
63
+
64
+ Args:
65
+ **kwargs: All arguments supported by agent.define_tool()
66
+ (name, description, parameters, handler, etc.)
67
+ """
68
+ # Merge swaig_fields with any explicitly passed fields
69
+ # Explicit fields take precedence over swaig_fields
70
+ merged_kwargs = dict(self.swaig_fields)
71
+ merged_kwargs.update(kwargs)
72
+
73
+ # Call the agent's define_tool with merged arguments
74
+ return self.agent.define_tool(**merged_kwargs)
75
+
56
76
 
57
77
 
58
78
  def get_hints(self) -> List[str]:
@@ -1937,9 +1937,13 @@
1937
1937
  {
1938
1938
  "type": "string",
1939
1939
  "const": "qwen3-235b-A22b-instruct"
1940
+ },
1941
+ {
1942
+ "type": "string",
1943
+ "const": "llama-3.1-8b-instruct-turbo@together.ai"
1940
1944
  }
1941
1945
  ],
1942
- "description": "The model to use for the AI. Allowed values are `gpt-4o-mini`, `gpt-4.1-mini`, `gpt-4.1-nano`, `nova-micro`, `nova-lite`, and `qwen3-235b-A22b-instruct`."
1946
+ "description": "The model to use for the AI. Allowed values are `gpt-4o-mini`, `gpt-4.1-mini`, `gpt-4.1-nano`, `nova-micro`, `nova-lite`, and `qwen3-235b-A22b-instruct` and `qwen3-4b-instruct-2507@brian`."
1943
1947
  },
1944
1948
  "ai_volume": {
1945
1949
  "anyOf": [
@@ -7663,4 +7667,4 @@
7663
7667
  }
7664
7668
  },
7665
7669
  "unevaluatedProperties": false
7666
- }
7670
+ }
@@ -88,9 +88,18 @@ class DocumentProcessor:
88
88
  ):
89
89
  """
90
90
  Initialize document processor
91
-
91
+
92
92
  Args:
93
- chunking_strategy: Strategy for chunking documents ('sentence', 'sliding', 'paragraph', 'page', 'semantic', 'topic', 'qa')
93
+ chunking_strategy: Strategy for chunking documents:
94
+ - 'sentence': Sentence-based chunking with overlap
95
+ - 'sliding': Sliding window with word-based chunks
96
+ - 'paragraph': Natural paragraph boundaries
97
+ - 'page': Page-based chunking (for PDFs)
98
+ - 'semantic': Semantic similarity-based chunking
99
+ - 'topic': Topic modeling-based chunking
100
+ - 'qa': Question-answer optimized chunking
101
+ - 'json': JSON structure-aware chunking
102
+ - 'markdown': Markdown structure-aware chunking with code block detection
94
103
  max_sentences_per_chunk: For sentence strategy (default: 5)
95
104
  chunk_size: For sliding strategy - words per chunk (default: 50)
96
105
  chunk_overlap: For sliding strategy - overlap in words (default: 10)
@@ -142,6 +151,9 @@ class DocumentProcessor:
142
151
  return self._chunk_by_qa_optimization(content, filename, file_type)
143
152
  elif self.chunking_strategy == 'json':
144
153
  return self._chunk_from_json(content, filename, file_type)
154
+ elif self.chunking_strategy == 'markdown':
155
+ # Use markdown-aware chunking for better structure preservation
156
+ return self._chunk_markdown_enhanced(content, filename)
145
157
  else:
146
158
  # Fallback to sentence-based chunking
147
159
  return self._chunk_by_sentences(content, filename, file_type)
@@ -339,75 +351,114 @@ class DocumentProcessor:
339
351
  return chunks
340
352
 
341
353
  def _chunk_markdown_enhanced(self, content: str, filename: str) -> List[Dict[str, Any]]:
342
- """Enhanced markdown chunking with better header handling"""
354
+ """Enhanced markdown chunking with code block detection and rich metadata
355
+
356
+ Features:
357
+ - Tracks header hierarchy for section paths
358
+ - Detects code blocks and extracts language
359
+ - Adds 'code' tags to chunks containing code
360
+ - Preserves markdown structure for better search
361
+ """
343
362
  chunks = []
344
363
  lines = content.split('\n')
345
-
364
+
346
365
  current_section = None
347
366
  current_hierarchy = [] # Track header hierarchy
348
367
  current_chunk = []
349
368
  current_size = 0
350
369
  line_start = 1
351
-
370
+ in_code_block = False
371
+ code_languages = [] # Track languages in current chunk
372
+ has_code = False
373
+
352
374
  for line_num, line in enumerate(lines, 1):
375
+ # Check for code block fences
376
+ code_fence_match = re.match(r'^```(\w+)?', line)
377
+ if code_fence_match:
378
+ in_code_block = not in_code_block
379
+ if in_code_block:
380
+ # Starting code block
381
+ has_code = True
382
+ lang = code_fence_match.group(1)
383
+ if lang and lang not in code_languages:
384
+ code_languages.append(lang)
385
+
353
386
  # Check for headers with hierarchy tracking
354
- header_match = re.match(r'^(#{1,6})\s+(.+)', line)
387
+ header_match = re.match(r'^(#{1,6})\s+(.+)', line) if not in_code_block else None
355
388
  if header_match:
356
389
  header_level = len(header_match.group(1))
357
390
  header_text = header_match.group(2).strip()
358
-
391
+
359
392
  # Save current chunk if it exists
360
393
  if current_chunk:
394
+ chunk_metadata = self._build_markdown_metadata(
395
+ current_hierarchy, code_languages, has_code
396
+ )
361
397
  chunks.append(self._create_chunk(
362
398
  content='\n'.join(current_chunk),
363
399
  filename=filename,
364
400
  section=self._build_section_path(current_hierarchy),
365
401
  start_line=line_start,
366
- end_line=line_num - 1
402
+ end_line=line_num - 1,
403
+ metadata=chunk_metadata
367
404
  ))
368
-
405
+
369
406
  # Update hierarchy
370
407
  current_hierarchy = current_hierarchy[:header_level-1] + [header_text]
371
408
  current_section = header_text
372
409
  current_chunk = [line]
373
410
  current_size = len(line)
374
411
  line_start = line_num
375
-
412
+ code_languages = []
413
+ has_code = False
414
+
376
415
  else:
377
416
  current_chunk.append(line)
378
417
  current_size += len(line) + 1
379
-
418
+
380
419
  # Check if chunk is getting too large - use smart splitting
381
- if current_size >= self.chunk_size:
420
+ # But don't split inside code blocks
421
+ if current_size >= self.chunk_size and not in_code_block:
382
422
  # Try to split at paragraph boundary first
383
423
  split_point = self._find_best_split_point(current_chunk)
384
-
424
+
385
425
  chunk_to_save = current_chunk[:split_point]
426
+ chunk_metadata = self._build_markdown_metadata(
427
+ current_hierarchy, code_languages, has_code
428
+ )
386
429
  chunks.append(self._create_chunk(
387
430
  content='\n'.join(chunk_to_save),
388
431
  filename=filename,
389
432
  section=self._build_section_path(current_hierarchy),
390
433
  start_line=line_start,
391
- end_line=line_start + split_point - 1
434
+ end_line=line_start + split_point - 1,
435
+ metadata=chunk_metadata
392
436
  ))
393
-
437
+
394
438
  # Start new chunk with overlap
395
439
  overlap_lines = self._get_overlap_lines(chunk_to_save)
396
440
  remaining_lines = current_chunk[split_point:]
397
441
  current_chunk = overlap_lines + remaining_lines
398
442
  current_size = sum(len(line) + 1 for line in current_chunk)
399
443
  line_start = line_start + split_point - len(overlap_lines)
400
-
444
+ # Reset code tracking for new chunk
445
+ code_languages = []
446
+ has_code = False
447
+
401
448
  # Add final chunk
402
449
  if current_chunk:
450
+ chunk_metadata = self._build_markdown_metadata(
451
+ current_hierarchy, code_languages, has_code
452
+ )
403
453
  chunks.append(self._create_chunk(
404
454
  content='\n'.join(current_chunk),
405
455
  filename=filename,
406
456
  section=self._build_section_path(current_hierarchy),
407
457
  start_line=line_start,
408
- end_line=len(lines)
458
+ end_line=len(lines),
459
+ metadata=chunk_metadata
409
460
  ))
410
-
461
+
411
462
  return chunks
412
463
 
413
464
  def _chunk_python_enhanced(self, content: str, filename: str) -> List[Dict[str, Any]]:
@@ -575,6 +626,49 @@ class DocumentProcessor:
575
626
  def _build_section_path(self, hierarchy: List[str]) -> str:
576
627
  """Build hierarchical section path from header hierarchy"""
577
628
  return ' > '.join(hierarchy) if hierarchy else None
629
+
630
+ def _build_markdown_metadata(self, hierarchy: List[str], code_languages: List[str], has_code: bool) -> Dict[str, Any]:
631
+ """Build rich metadata for markdown chunks
632
+
633
+ Args:
634
+ hierarchy: Current header hierarchy (e.g., ['Installation', 'Requirements', 'Python'])
635
+ code_languages: List of code block languages found in chunk (e.g., ['python', 'bash'])
636
+ has_code: Whether chunk contains any code blocks
637
+
638
+ Returns:
639
+ Dictionary with markdown-specific metadata including tags
640
+ """
641
+ metadata = {
642
+ 'chunk_type': 'markdown',
643
+ }
644
+
645
+ # Add header level metadata
646
+ if hierarchy:
647
+ for i, header in enumerate(hierarchy, 1):
648
+ metadata[f'h{i}'] = header
649
+
650
+ # Add code-related metadata
651
+ if has_code:
652
+ metadata['has_code'] = True
653
+ if code_languages:
654
+ metadata['code_languages'] = code_languages
655
+
656
+ # Build tags for enhanced searching
657
+ tags = []
658
+ if has_code:
659
+ tags.append('code')
660
+ # Add language-specific tags
661
+ for lang in code_languages:
662
+ tags.append(f'code:{lang}')
663
+
664
+ # Add tags for header levels (searchable by section depth)
665
+ if len(hierarchy) > 0:
666
+ tags.append(f'depth:{len(hierarchy)}')
667
+
668
+ if tags:
669
+ metadata['tags'] = tags
670
+
671
+ return metadata
578
672
 
579
673
  def _build_python_section(self, class_name: Optional[str], function_name: Optional[str]) -> str:
580
674
  """Build section name for Python code"""
@@ -114,51 +114,48 @@ class SearchEngine:
114
114
  logger.error(f"Error converting query vector: {e}")
115
115
  return self._keyword_search_only(enhanced_text, count, tags, original_query)
116
116
 
117
- # Stage 1: Collect candidates using fast methods
117
+ # HYBRID APPROACH: Search vector AND metadata in parallel
118
+ # Stage 1: Run both search types simultaneously
119
+ search_multiplier = 3
120
+
121
+ # Vector search (semantic similarity - primary ranking signal)
122
+ vector_results = self._vector_search(query_array, count * search_multiplier)
123
+
124
+ # Metadata/keyword searches (confirmation signals and backfill)
125
+ filename_results = self._filename_search(original_query or enhanced_text, count * search_multiplier)
126
+ metadata_results = self._metadata_search(original_query or enhanced_text, count * search_multiplier)
127
+ keyword_results = self._keyword_search(enhanced_text, count * search_multiplier, original_query)
128
+
129
+ logger.debug(f"Parallel search: vector={len(vector_results)}, filename={len(filename_results)}, "
130
+ f"metadata={len(metadata_results)}, keyword={len(keyword_results)}")
131
+
132
+ # Stage 2: Merge all results into candidate pool
118
133
  candidates = {}
119
-
120
- # Fast searches - collect all potential matches
121
- filename_results = self._filename_search(original_query or enhanced_text, count * 3)
122
- metadata_results = self._metadata_search(original_query or enhanced_text, count * 2)
123
- keyword_results = self._keyword_search(enhanced_text, count * 2, original_query)
124
-
125
- logger.debug(f"Search for '{original_query}': filename={len(filename_results)}, metadata={len(metadata_results)}, keyword={len(keyword_results)}")
126
-
127
- # Merge candidates from different sources
128
- for result_set, source_weight in [(filename_results, 2.0),
129
- (metadata_results, 1.5),
130
- (keyword_results, 1.0)]:
134
+
135
+ # Add vector results first (primary signal)
136
+ for result in vector_results:
137
+ chunk_id = result['id']
138
+ candidates[chunk_id] = result
139
+ candidates[chunk_id]['vector_score'] = result['score']
140
+ candidates[chunk_id]['vector_distance'] = 1 - result['score']
141
+ candidates[chunk_id]['sources'] = {'vector': True}
142
+ candidates[chunk_id]['source_scores'] = {'vector': result['score']}
143
+
144
+ # Add metadata/keyword results (secondary signals that boost or backfill)
145
+ for result_set, source_type, source_weight in [(filename_results, 'filename', 2.0),
146
+ (metadata_results, 'metadata', 1.5),
147
+ (keyword_results, 'keyword', 1.0)]:
131
148
  for result in result_set:
132
149
  chunk_id = result['id']
133
150
  if chunk_id not in candidates:
151
+ # New candidate from metadata/keyword (no vector match)
134
152
  candidates[chunk_id] = result
135
- candidates[chunk_id]['sources'] = {}
136
- candidates[chunk_id]['source_scores'] = {}
137
-
138
- # Track which searches found this chunk
139
- candidates[chunk_id]['sources'][result['search_type']] = True
140
- candidates[chunk_id]['source_scores'][result['search_type']] = result['score'] * source_weight
141
-
142
- # Stage 2: Check if we have enough candidates
143
- if len(candidates) < count * 2:
144
- # Not enough candidates from fast searches - add full vector search
145
- logger.debug(f"Only {len(candidates)} candidates from fast search, adding full vector search")
146
- vector_results = self._vector_search(query_array, count * 3)
147
-
148
- for result in vector_results:
149
- chunk_id = result['id']
150
- if chunk_id not in candidates:
151
- candidates[chunk_id] = result
152
- candidates[chunk_id]['sources'] = {'vector': True}
153
- candidates[chunk_id]['source_scores'] = {}
154
-
155
- # Add vector score
156
- candidates[chunk_id]['vector_score'] = result['score']
157
- candidates[chunk_id]['vector_distance'] = 1 - result['score']
158
- else:
159
- # We have enough candidates - just re-rank them with vectors
160
- logger.debug(f"Re-ranking {len(candidates)} candidates with vector similarity")
161
- self._add_vector_scores_to_candidates(candidates, query_array, distance_threshold)
153
+ candidates[chunk_id]['sources'] = {source_type: True}
154
+ candidates[chunk_id]['source_scores'] = {source_type: result['score'] * source_weight}
155
+ else:
156
+ # Exists in vector results - add metadata/keyword as confirmation signal
157
+ candidates[chunk_id]['sources'][source_type] = True
158
+ candidates[chunk_id]['source_scores'][source_type] = result['score'] * source_weight
162
159
 
163
160
  # Stage 3: Score and rank all candidates
164
161
  final_results = []
@@ -190,12 +187,12 @@ class SearchEngine:
190
187
 
191
188
  # Apply diversity penalties to prevent single-file dominance
192
189
  final_results = self._apply_diversity_penalties(final_results, count)
193
-
190
+
194
191
  # Ensure 'score' field exists for CLI compatibility
195
192
  for r in final_results:
196
193
  if 'score' not in r:
197
194
  r['score'] = r.get('final_score', 0.0)
198
-
195
+
199
196
  return final_results[:count]
200
197
 
201
198
  def _keyword_search_only(self, enhanced_text: str, count: int,
@@ -1038,70 +1035,55 @@ class SearchEngine:
1038
1035
  logger.error(f"Error in vector re-ranking: {e}")
1039
1036
 
1040
1037
  def _calculate_combined_score(self, candidate: Dict, distance_threshold: float) -> float:
1041
- """Calculate final score combining all signals with comprehensive match bonus"""
1042
- # Base scores from different sources
1043
- source_scores = candidate.get('source_scores', {})
1044
-
1045
- # Check for comprehensive matching (multiple signals)
1038
+ """Calculate final score with hybrid vector + metadata weighting
1039
+
1040
+ Hybrid approach:
1041
+ - Vector score is the primary ranking signal (semantic similarity)
1042
+ - Metadata/keyword matches provide confirmation boost
1043
+ - Multiple signal types indicate high relevance (confirmation bonus)
1044
+ - Special boost for 'code' tag matches when query contains code-related terms
1045
+ """
1046
1046
  sources = candidate.get('sources', {})
1047
- num_sources = len(sources)
1048
-
1049
- # Get match coverage information
1050
- match_coverage = candidate.get('match_coverage', 0)
1051
- fields_matched = candidate.get('fields_matched', 0)
1052
-
1053
- # Calculate base score with exponential boost for multiple sources
1054
- if num_sources > 1:
1055
- # Multiple signal matches are exponentially better
1056
- multi_signal_boost = 1.0 + (0.3 * (num_sources - 1))
1057
- base_score = sum(source_scores.values()) * multi_signal_boost
1058
- else:
1059
- base_score = sum(source_scores.values())
1060
-
1061
- # Apply comprehensive match bonus
1062
- if match_coverage > 0.5: # More than 50% of query terms matched
1063
- coverage_bonus = 1.0 + (match_coverage - 0.5) * 0.5
1064
- base_score *= coverage_bonus
1065
-
1066
- # Apply field diversity bonus (matching in multiple metadata fields)
1067
- if fields_matched > 2:
1068
- field_bonus = 1.0 + (fields_matched - 2) * 0.1
1069
- base_score *= field_bonus
1070
-
1071
- # Apply vector similarity multiplier if available
1047
+ source_scores = candidate.get('source_scores', {})
1048
+
1049
+ # Vector score is PRIMARY
1072
1050
  if 'vector_score' in candidate:
1073
1051
  vector_score = candidate['vector_score']
1074
- vector_distance = candidate.get('vector_distance', 1 - vector_score)
1075
-
1076
- # Distance-aware scoring
1077
- if distance_threshold > 0:
1078
- if vector_distance <= distance_threshold:
1079
- # Within threshold - full vector score
1080
- vector_multiplier = vector_score
1081
- elif vector_distance <= distance_threshold * 1.5:
1082
- # Near threshold - gradual decay
1083
- overflow = (vector_distance - distance_threshold) / (distance_threshold * 0.5)
1084
- vector_multiplier = vector_score * (1 - overflow * 0.3)
1085
- else:
1086
- # Beyond threshold - minimal contribution
1087
- vector_multiplier = vector_score * 0.3
1088
- else:
1089
- vector_multiplier = vector_score
1090
-
1091
- # For chunks found by vector-only search, use vector score directly
1092
- if 'vector' in sources and len(sources) == 1:
1093
- base_score = vector_score
1094
- else:
1095
- # For chunks found by multiple methods, apply vector as quality check
1096
- base_score *= vector_multiplier
1097
-
1098
- # Special handling for strong metadata matches
1099
- if 'metadata' in sources:
1100
- metadata_matches = candidate.get('metadata_matches', {})
1101
- # Strong category or product match should boost significantly
1102
- if metadata_matches.get('category', 0) > 0.8 or metadata_matches.get('product', 0) > 0.8:
1103
- base_score *= 1.2
1104
-
1052
+ base_score = vector_score
1053
+
1054
+ # Metadata/keyword matches provide confirmation boost
1055
+ if len(sources) > 1:
1056
+ # Has both vector AND metadata/keyword matches - strong confirmation signal
1057
+ keyword_signals = sum(source_scores.get(k, 0) for k in ['keyword', 'filename', 'metadata'])
1058
+ if keyword_signals > 0:
1059
+ # Normalize and apply boost (up to 30% for strong confirmation)
1060
+ keyword_boost = min(0.3, keyword_signals * 0.15)
1061
+ base_score = vector_score * (1.0 + keyword_boost)
1062
+
1063
+ # Additional boost if multiple signal types confirm (2+ sources)
1064
+ num_metadata_sources = sum(1 for s in ['keyword', 'filename', 'metadata'] if s in sources)
1065
+ if num_metadata_sources >= 2:
1066
+ # Multiple confirmation signals - very high confidence
1067
+ base_score *= 1.1
1068
+
1069
+ # Check for code-related tags to boost code examples
1070
+ tags = candidate.get('metadata', {}).get('tags', [])
1071
+ if 'code' in tags:
1072
+ # This chunk contains code - boost if query is code-related
1073
+ # (metadata search would have found it if query mentioned code/example/python/etc)
1074
+ if 'metadata' in sources or 'keyword' in sources:
1075
+ # Query matched code-related metadata - apply code boost
1076
+ base_score *= 1.2
1077
+ else:
1078
+ # No vector score - this is a keyword-only result (backfill)
1079
+ # Use keyword scores but penalize for lack of semantic match
1080
+ base_score = sum(source_scores.values()) * 0.6 # 40% penalty for no vector
1081
+
1082
+ # Still boost code chunks if metadata matched
1083
+ tags = candidate.get('metadata', {}).get('tags', [])
1084
+ if 'code' in tags and 'metadata' in sources:
1085
+ base_score *= 1.15
1086
+
1105
1087
  return base_score
1106
1088
 
1107
1089
  def _apply_diversity_penalties(self, results: List[Dict], target_count: int) -> List[Dict]:
@@ -1166,7 +1148,65 @@ class SearchEngine:
1166
1148
  penalized_results[:target_count] = selected
1167
1149
 
1168
1150
  return penalized_results
1169
-
1151
+
1152
+ def _apply_match_type_diversity(self, results: List[Dict], target_count: int) -> List[Dict]:
1153
+ """Ensure diversity of match types in final results
1154
+
1155
+ Ensures we have a mix of:
1156
+ - Vector-only matches (semantic similarity, good for code examples)
1157
+ - Keyword-only matches (exact term matches)
1158
+ - Hybrid matches (both vector + keyword/metadata)
1159
+ """
1160
+ if not results or len(results) <= target_count:
1161
+ return results
1162
+
1163
+ # Categorize results by match type
1164
+ vector_only = []
1165
+ keyword_only = []
1166
+ hybrid = []
1167
+
1168
+ for result in results:
1169
+ sources = result.get('sources', {})
1170
+ has_vector = 'vector' in sources
1171
+ has_keyword = any(k in sources for k in ['keyword', 'filename', 'metadata'])
1172
+
1173
+ if has_vector and not has_keyword:
1174
+ vector_only.append(result)
1175
+ elif has_keyword and not has_vector:
1176
+ keyword_only.append(result)
1177
+ else:
1178
+ hybrid.append(result)
1179
+
1180
+ # Build diverse result set
1181
+ # Target distribution: 40% hybrid, 40% vector-only, 20% keyword-only
1182
+ # This ensures we include semantic matches (code examples) even if keywords don't match
1183
+ diversified = []
1184
+
1185
+ # Take top hybrid matches first (best overall)
1186
+ hybrid_target = max(1, int(target_count * 0.4))
1187
+ diversified.extend(hybrid[:hybrid_target])
1188
+
1189
+ # Ensure we have vector-only matches (critical for code examples)
1190
+ vector_target = max(1, int(target_count * 0.4))
1191
+ diversified.extend(vector_only[:vector_target])
1192
+
1193
+ # Add keyword-only matches
1194
+ keyword_target = max(1, int(target_count * 0.2))
1195
+ diversified.extend(keyword_only[:keyword_target])
1196
+
1197
+ # Fill remaining slots with best remaining results regardless of type
1198
+ remaining_slots = target_count - len(diversified)
1199
+ if remaining_slots > 0:
1200
+ # Get all unused results
1201
+ used_ids = set(r['id'] for r in diversified)
1202
+ unused = [r for r in results if r['id'] not in used_ids]
1203
+ diversified.extend(unused[:remaining_slots])
1204
+
1205
+ # Sort by final score to maintain quality ordering
1206
+ diversified.sort(key=lambda x: x['final_score'], reverse=True)
1207
+
1208
+ return diversified
1209
+
1170
1210
  def get_stats(self) -> Dict[str, Any]:
1171
1211
  """Get statistics about the search index"""
1172
1212
  # Use pgvector backend if available
@@ -159,7 +159,7 @@ class DataSphereSkill(SkillBase):
159
159
 
160
160
  def register_tools(self) -> None:
161
161
  """Register knowledge search tool with the agent"""
162
- self.agent.define_tool(
162
+ self.define_tool(
163
163
  name=self.tool_name,
164
164
  description="Search the knowledge base for information on any topic and return relevant results",
165
165
  parameters={
@@ -168,8 +168,7 @@ class DataSphereSkill(SkillBase):
168
168
  "description": "The search query - what information you're looking for in the knowledge base"
169
169
  }
170
170
  },
171
- handler=self._search_knowledge_handler,
172
- **self.swaig_fields
171
+ handler=self._search_knowledge_handler
173
172
  )
174
173
 
175
174
  def _search_knowledge_handler(self, args, raw_data):
@@ -30,7 +30,7 @@ class DateTimeSkill(SkillBase):
30
30
  def register_tools(self) -> None:
31
31
  """Register datetime tools with the agent"""
32
32
 
33
- self.agent.define_tool(
33
+ self.define_tool(
34
34
  name="get_current_time",
35
35
  description="Get the current time, optionally in a specific timezone",
36
36
  parameters={
@@ -39,11 +39,10 @@ class DateTimeSkill(SkillBase):
39
39
  "description": "Timezone name (e.g., 'America/New_York', 'Europe/London'). Defaults to UTC."
40
40
  }
41
41
  },
42
- handler=self._get_time_handler,
43
- **self.swaig_fields
42
+ handler=self._get_time_handler
44
43
  )
45
44
 
46
- self.agent.define_tool(
45
+ self.define_tool(
47
46
  name="get_current_date",
48
47
  description="Get the current date",
49
48
  parameters={
@@ -52,8 +51,7 @@ class DateTimeSkill(SkillBase):
52
51
  "description": "Timezone name for the date. Defaults to UTC."
53
52
  }
54
53
  },
55
- handler=self._get_date_handler,
56
- **self.swaig_fields
54
+ handler=self._get_date_handler
57
55
  )
58
56
 
59
57
  def _get_time_handler(self, args, raw_data):
@@ -29,7 +29,7 @@ class MathSkill(SkillBase):
29
29
  def register_tools(self) -> None:
30
30
  """Register math tools with the agent"""
31
31
 
32
- self.agent.define_tool(
32
+ self.define_tool(
33
33
  name="calculate",
34
34
  description="Perform a mathematical calculation with basic operations (+, -, *, /, %, **)",
35
35
  parameters={
@@ -38,8 +38,7 @@ class MathSkill(SkillBase):
38
38
  "description": "Mathematical expression to evaluate (e.g., '2 + 3 * 4', '(10 + 5) / 3')"
39
39
  }
40
40
  },
41
- handler=self._calculate_handler,
42
- **self.swaig_fields
41
+ handler=self._calculate_handler
43
42
  )
44
43
 
45
44
  def _calculate_handler(self, args, raw_data):
@@ -215,7 +215,7 @@ class MCPGatewaySkill(SkillBase):
215
215
  self.logger.error(f"Failed to get tools for service '{service_name}': {e}")
216
216
 
217
217
  # Register the hangup hook for session cleanup
218
- self.agent.define_tool(
218
+ self.define_tool(
219
219
  name="_mcp_gateway_hangup",
220
220
  description="Internal cleanup function for MCP sessions",
221
221
  parameters={},
@@ -260,7 +260,7 @@ class MCPGatewaySkill(SkillBase):
260
260
  return self._call_mcp_tool(service_name, tool_name, args, raw_data)
261
261
 
262
262
  # Register the SWAIG function
263
- self.agent.define_tool(
263
+ self.define_tool(
264
264
  name=swaig_name,
265
265
  description=f"[{service_name}] {tool_def.get('description', tool_name)}",
266
266
  parameters=swaig_params,
@@ -136,6 +136,13 @@ class NativeVectorSearchSkill(SkillBase):
136
136
  "default": "",
137
137
  "required": False
138
138
  },
139
+ "max_content_length": {
140
+ "type": "integer",
141
+ "description": "Maximum total response size in characters (distributed across all results)",
142
+ "default": 32768,
143
+ "required": False,
144
+ "minimum": 1000
145
+ },
139
146
  "response_format_callback": {
140
147
  "type": "callable",
141
148
  "description": "Optional callback function to format/transform the response. Called with (response, agent, query, results, args). Must return a string.",
@@ -251,6 +258,7 @@ class NativeVectorSearchSkill(SkillBase):
251
258
  )
252
259
  self.response_prefix = self.params.get('response_prefix', '')
253
260
  self.response_postfix = self.params.get('response_postfix', '')
261
+ self.max_content_length = self.params.get('max_content_length', 32768)
254
262
  self.response_format_callback = self.params.get('response_format_callback')
255
263
  self.keyword_weight = self.params.get('keyword_weight')
256
264
  self.model_name = self.params.get('model_name', 'mini')
@@ -274,8 +282,8 @@ class NativeVectorSearchSkill(SkillBase):
274
282
  if parsed.path:
275
283
  self.remote_base_url += parsed.path
276
284
 
277
- # SWAIG fields for function fillers
278
- self.swaig_fields = self.params.get('swaig_fields', {})
285
+ # SWAIG fields are already extracted by SkillBase.__init__()
286
+ # No need to re-fetch from params - use self.swaig_fields inherited from parent
279
287
 
280
288
  # **EARLY REMOTE CHECK - Option 1**
281
289
  # If remote URL is configured, skip all heavy local imports and just validate remote connectivity
@@ -460,7 +468,7 @@ class NativeVectorSearchSkill(SkillBase):
460
468
  'Search the local knowledge base for information'
461
469
  )
462
470
 
463
- self.agent.define_tool(
471
+ self.define_tool(
464
472
  name=self.tool_name,
465
473
  description=description,
466
474
  parameters={
@@ -474,8 +482,7 @@ class NativeVectorSearchSkill(SkillBase):
474
482
  "default": self.count
475
483
  }
476
484
  },
477
- handler=self._search_handler,
478
- **self.swaig_fields
485
+ handler=self._search_handler
479
486
  )
480
487
 
481
488
  # Add our tool to the Knowledge Search section
@@ -601,21 +608,36 @@ class NativeVectorSearchSkill(SkillBase):
601
608
 
602
609
  return SwaigFunctionResult(no_results_msg)
603
610
 
604
- # Format results
611
+ # Format results with dynamic per-result truncation
605
612
  response_parts = []
606
-
613
+
607
614
  # Add response prefix if configured
608
615
  if self.response_prefix:
609
616
  response_parts.append(self.response_prefix)
610
-
617
+
611
618
  response_parts.append(f"Found {len(results)} relevant results for '{query}':\n")
612
-
619
+
620
+ # Calculate per-result content budget
621
+ # Estimate overhead per result: metadata (~200 chars) + formatting (~100 chars)
622
+ estimated_overhead_per_result = 300
623
+ # Account for prefix/postfix/header in total overhead
624
+ prefix_postfix_overhead = len(self.response_prefix) + len(self.response_postfix) + 100
625
+ total_overhead = (len(results) * estimated_overhead_per_result) + prefix_postfix_overhead
626
+ available_for_content = self.max_content_length - total_overhead
627
+
628
+ # Ensure minimum of 500 chars per result
629
+ per_result_limit = max(500, available_for_content // len(results)) if len(results) > 0 else 1000
630
+
613
631
  for i, result in enumerate(results, 1):
614
632
  filename = result['metadata']['filename']
615
633
  section = result['metadata'].get('section', '')
616
634
  score = result['score']
617
635
  content = result['content']
618
-
636
+
637
+ # Truncate content to per-result limit
638
+ if len(content) > per_result_limit:
639
+ content = content[:per_result_limit] + "..."
640
+
619
641
  # Get tags from either top level or metadata
620
642
  tags = result.get('tags', [])
621
643
  if not tags and 'metadata' in result['metadata'] and 'tags' in result['metadata']['metadata']:
@@ -624,16 +646,16 @@ class NativeVectorSearchSkill(SkillBase):
624
646
  elif not tags and 'tags' in result['metadata']:
625
647
  # Check in metadata directly
626
648
  tags = result['metadata']['tags']
627
-
649
+
628
650
  result_text = f"**Result {i}** (from {filename}"
629
651
  if section:
630
652
  result_text += f", section: {section}"
631
653
  if tags:
632
654
  result_text += f", tags: {', '.join(tags)}"
633
655
  result_text += f", relevance: {score:.2f})\n{content}\n"
634
-
656
+
635
657
  response_parts.append(result_text)
636
-
658
+
637
659
  # Add response postfix if configured
638
660
  if self.response_postfix:
639
661
  response_parts.append(self.response_postfix)
@@ -223,7 +223,7 @@ class SpiderSkill(SkillBase):
223
223
  tool_prefix = f"{tool_prefix}_"
224
224
 
225
225
  # Register scrape_url tool
226
- self.agent.define_tool(
226
+ self.define_tool(
227
227
  name=f"{tool_prefix}scrape_url",
228
228
  description="Extract text content from a single web page",
229
229
  parameters={
@@ -233,12 +233,11 @@ class SpiderSkill(SkillBase):
233
233
  }
234
234
  },
235
235
  required=["url"],
236
- handler=self._scrape_url_handler,
237
- **self.swaig_fields
236
+ handler=self._scrape_url_handler
238
237
  )
239
238
 
240
239
  # Register crawl_site tool
241
- self.agent.define_tool(
240
+ self.define_tool(
242
241
  name=f"{tool_prefix}crawl_site",
243
242
  description="Crawl multiple pages starting from a URL",
244
243
  parameters={
@@ -248,12 +247,11 @@ class SpiderSkill(SkillBase):
248
247
  }
249
248
  },
250
249
  required=["start_url"],
251
- handler=self._crawl_site_handler,
252
- **self.swaig_fields
250
+ handler=self._crawl_site_handler
253
251
  )
254
252
 
255
253
  # Register extract_structured_data tool
256
- self.agent.define_tool(
254
+ self.define_tool(
257
255
  name=f"{tool_prefix}extract_structured_data",
258
256
  description="Extract specific data from a web page using selectors",
259
257
  parameters={
@@ -263,8 +261,7 @@ class SpiderSkill(SkillBase):
263
261
  }
264
262
  },
265
263
  required=["url"],
266
- handler=self._extract_structured_handler,
267
- **self.swaig_fields
264
+ handler=self._extract_structured_handler
268
265
  )
269
266
 
270
267
  def _fetch_url(self, url: str) -> Optional[requests.Response]:
@@ -21,7 +21,7 @@ from signalwire_agents.core.function_result import SwaigFunctionResult
21
21
  class GoogleSearchScraper:
22
22
  """Google Search and Web Scraping functionality"""
23
23
 
24
- def __init__(self, api_key: str, search_engine_id: str, max_content_length: int = 2000):
24
+ def __init__(self, api_key: str, search_engine_id: str, max_content_length: int = 32768):
25
25
  self.api_key = api_key
26
26
  self.search_engine_id = search_engine_id
27
27
  self.max_content_length = max_content_length
@@ -62,63 +62,84 @@ class GoogleSearchScraper:
62
62
  except Exception as e:
63
63
  return []
64
64
 
65
- def extract_text_from_url(self, url: str, timeout: int = 10) -> str:
66
- """Scrape a URL and extract readable text content"""
65
+ def extract_text_from_url(self, url: str, content_limit: int = None, timeout: int = 10) -> str:
66
+ """Scrape a URL and extract readable text content
67
+
68
+ Args:
69
+ url: URL to scrape
70
+ content_limit: Maximum characters to return (uses self.max_content_length if not provided)
71
+ timeout: Request timeout in seconds
72
+ """
67
73
  try:
68
74
  response = self.session.get(url, timeout=timeout)
69
75
  response.raise_for_status()
70
-
76
+
71
77
  soup = BeautifulSoup(response.content, 'html.parser')
72
-
78
+
73
79
  # Remove unwanted elements
74
80
  for script in soup(["script", "style", "nav", "footer", "header", "aside"]):
75
81
  script.decompose()
76
-
82
+
77
83
  text = soup.get_text()
78
-
84
+
79
85
  # Clean up the text
80
86
  lines = (line.strip() for line in text.splitlines())
81
87
  chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
82
88
  text = ' '.join(chunk for chunk in chunks if chunk)
83
-
89
+
84
90
  # Limit text length
85
- if len(text) > self.max_content_length:
86
- text = text[:self.max_content_length] + "... [Content truncated]"
87
-
91
+ limit = content_limit if content_limit is not None else self.max_content_length
92
+ if len(text) > limit:
93
+ text = text[:limit]
94
+
88
95
  return text
89
-
96
+
90
97
  except Exception as e:
91
98
  return ""
92
99
 
93
100
  def search_and_scrape(self, query: str, num_results: int = 3, delay: float = 0.5) -> str:
94
- """Main function: search Google and scrape the resulting pages"""
101
+ """Main function: search Google and scrape the resulting pages
102
+
103
+ Dynamically calculates per-result content limit based on total max_content_length
104
+ and number of results to ensure total response stays within bounds.
105
+ """
95
106
  search_results = self.search_google(query, num_results)
96
-
107
+
97
108
  if not search_results:
98
109
  return f"No search results found for query: {query}"
99
-
110
+
111
+ # Calculate per-result content budget
112
+ # Reserve ~300 chars per result for overhead (titles, URLs, snippets, formatting)
113
+ estimated_overhead_per_result = 300
114
+ total_overhead = num_results * estimated_overhead_per_result
115
+ available_for_content = self.max_content_length - total_overhead
116
+
117
+ # Ensure we have at least 1000 chars per result
118
+ per_result_limit = max(1000, available_for_content // num_results)
119
+
100
120
  all_text = []
101
-
121
+
102
122
  for i, result in enumerate(search_results, 1):
103
123
  text_content = f"=== RESULT {i} ===\n"
104
124
  text_content += f"Title: {result['title']}\n"
105
125
  text_content += f"URL: {result['url']}\n"
106
126
  text_content += f"Snippet: {result['snippet']}\n"
107
127
  text_content += f"Content:\n"
108
-
109
- page_text = self.extract_text_from_url(result['url'])
110
-
128
+
129
+ # Pass the calculated per-result limit
130
+ page_text = self.extract_text_from_url(result['url'], content_limit=per_result_limit)
131
+
111
132
  if page_text:
112
133
  text_content += page_text
113
134
  else:
114
135
  text_content += "Failed to extract content from this page."
115
-
136
+
116
137
  text_content += f"\n{'='*50}\n\n"
117
138
  all_text.append(text_content)
118
-
139
+
119
140
  if i < len(search_results):
120
141
  time.sleep(delay)
121
-
142
+
122
143
  return '\n'.join(all_text)
123
144
 
124
145
 
@@ -163,7 +184,7 @@ class WebSearchSkill(SkillBase):
163
184
  # Set default parameters
164
185
  self.default_num_results = self.params.get('num_results', 1)
165
186
  self.default_delay = self.params.get('delay', 0)
166
- self.max_content_length = self.params.get('max_content_length', 2000)
187
+ self.max_content_length = self.params.get('max_content_length', 32768)
167
188
  self.no_results_message = self.params.get('no_results_message',
168
189
  "I couldn't find any results for '{query}'. "
169
190
  "This might be due to a very specific query or temporary issues. "
@@ -184,7 +205,7 @@ class WebSearchSkill(SkillBase):
184
205
 
185
206
  def register_tools(self) -> None:
186
207
  """Register web search tool with the agent"""
187
- self.agent.define_tool(
208
+ self.define_tool(
188
209
  name=self.tool_name,
189
210
  description="Search the web for information on any topic and return detailed results with content from multiple sources",
190
211
  parameters={
@@ -193,8 +214,7 @@ class WebSearchSkill(SkillBase):
193
214
  "description": "The search query - what you want to find information about"
194
215
  }
195
216
  },
196
- handler=self._web_search_handler,
197
- **self.swaig_fields
217
+ handler=self._web_search_handler
198
218
  )
199
219
 
200
220
  def _web_search_handler(self, args, raw_data):
@@ -308,10 +328,10 @@ class WebSearchSkill(SkillBase):
308
328
  },
309
329
  "max_content_length": {
310
330
  "type": "integer",
311
- "description": "Maximum content length per scraped page (characters)",
312
- "default": 2000,
331
+ "description": "Maximum total response size in characters (distributed across all results)",
332
+ "default": 32768,
313
333
  "required": False,
314
- "min": 100
334
+ "min": 1000
315
335
  },
316
336
  "no_results_message": {
317
337
  "type": "string",
@@ -321,4 +341,4 @@ class WebSearchSkill(SkillBase):
321
341
  }
322
342
  })
323
343
 
324
- return schema
344
+ return schema
@@ -84,7 +84,7 @@ class WikipediaSearchSkill(SkillBase):
84
84
  """
85
85
  Register the SWAIG tool for Wikipedia search.
86
86
  """
87
- self.agent.define_tool(
87
+ self.define_tool(
88
88
  name="search_wiki",
89
89
  description="Search Wikipedia for information about a topic and get article summaries",
90
90
  parameters={
@@ -93,8 +93,7 @@ class WikipediaSearchSkill(SkillBase):
93
93
  "description": "The search term or topic to look up on Wikipedia"
94
94
  }
95
95
  },
96
- handler=self._search_wiki_handler,
97
- **self.swaig_fields
96
+ handler=self._search_wiki_handler
98
97
  )
99
98
 
100
99
  def _search_wiki_handler(self, args, raw_data):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalwire_agents
3
- Version: 0.1.50
3
+ Version: 0.1.53
4
4
  Summary: SignalWire AI Agents SDK
5
5
  Author-email: SignalWire Team <info@signalwire.com>
6
6
  License: MIT
@@ -18,16 +18,16 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Requires-Python: >=3.7
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
- Requires-Dist: fastapi==0.115.12
22
- Requires-Dist: pydantic==2.11.4
23
- Requires-Dist: PyYAML==6.0.2
24
- Requires-Dist: Requests==2.32.3
25
- Requires-Dist: setuptools==66.1.1
26
- Requires-Dist: signalwire_pom==2.7.1
27
- Requires-Dist: structlog==25.3.0
28
- Requires-Dist: uvicorn==0.34.2
29
- Requires-Dist: beautifulsoup4==4.12.3
30
- Requires-Dist: pytz==2023.3
21
+ Requires-Dist: fastapi>=0.115.12
22
+ Requires-Dist: pydantic>=2.11.4
23
+ Requires-Dist: PyYAML>=6.0.2
24
+ Requires-Dist: Requests>=2.32.3
25
+ Requires-Dist: setuptools>=66.1.1
26
+ Requires-Dist: signalwire_pom>=2.7.1
27
+ Requires-Dist: structlog>=25.3.0
28
+ Requires-Dist: uvicorn>=0.34.2
29
+ Requires-Dist: beautifulsoup4>=4.12.3
30
+ Requires-Dist: pytz>=2023.3
31
31
  Requires-Dist: lxml>=4.9.0
32
32
  Provides-Extra: search-queryonly
33
33
  Requires-Dist: numpy>=1.24.0; extra == "search-queryonly"
@@ -1,9 +1,9 @@
1
- signalwire_agents/__init__.py,sha256=PDuehLVjqOYw3h-LyVMikp-NYwtildUWXj56x4ri9cg,5031
1
+ signalwire_agents/__init__.py,sha256=FNhNMJEBAOvXnnAw0S04cqxLRF4kW0uoASgIc6sglMs,5031
2
2
  signalwire_agents/agent_server.py,sha256=x9HyWia8D3r6KMqY-Q4DtNVivfJWLTx8B-KzUI8okuA,26880
3
- signalwire_agents/schema.json,sha256=D0Ui-VdLKNdMO8aYQBX_2NM3_JPnuhdVzhbLAPAWG1c,240423
3
+ signalwire_agents/schema.json,sha256=rxXtjWsyCKokj0ONGIs0k-VnWMJrbR2w2ZsYoQmYjCc,240585
4
4
  signalwire_agents/agents/bedrock.py,sha256=J582gooNtxtep4xdVOfyDzRtHp_XrurPMS93xf2Xod0,10836
5
5
  signalwire_agents/cli/__init__.py,sha256=XbxAQFaCIdGXIXJiriVBWoFPOJsC401u21588nO4TG8,388
6
- signalwire_agents/cli/build_search.py,sha256=Yh5hNM0ur88UMuKo5ZDoN_bAzBGpj2RG1Ys1_3xlfUc,54144
6
+ signalwire_agents/cli/build_search.py,sha256=_DcwRcrlr9uwmrI7FYp6WZT9aSU6KWgZmtcnimf1YnA,54925
7
7
  signalwire_agents/cli/config.py,sha256=2i4e0BArdKsaXxjeueYYRNke7GWicHPYC2wuitVrP7A,2541
8
8
  signalwire_agents/cli/swaig_test_wrapper.py,sha256=t63HQpEc1Up5AcysEHP1OsEQcgSMKH-9H1L2IhFso18,1533
9
9
  signalwire_agents/cli/test_swaig.py,sha256=-v-XjTUWZNxmMJuOF5_cB1Jz8x8emJoqgqS_8jLeT4Y,31487
@@ -33,7 +33,7 @@ signalwire_agents/core/function_result.py,sha256=4CcbxwstlSRUQtbCty2evewvNZP35dW
33
33
  signalwire_agents/core/logging_config.py,sha256=x4d_RAjBjVpJOFA2vXnPP2dNr13BZHz091J5rGpC77Y,13142
34
34
  signalwire_agents/core/pom_builder.py,sha256=ywuiIfP8BeLBPo_G4X1teZlG6zTCMkW71CZnmyoDTAQ,6636
35
35
  signalwire_agents/core/security_config.py,sha256=iAnAzKEJQiXL6mMpDaYm3Sjkxwm4x2N9HD6DeWSI8yI,12536
36
- signalwire_agents/core/skill_base.py,sha256=1b_4ht_T1BVnfzHYqoILb3idrrPYMs5-G-adHo2IVss,6903
36
+ signalwire_agents/core/skill_base.py,sha256=kCgzvlA9uu3CblLAigYuM-0LL-n0xEEoXV-JmNn-Beo,7795
37
37
  signalwire_agents/core/skill_manager.py,sha256=D4erpz0tmSYLqyfeteNNIY0VRWDtX0rDw3n7Z_f0W5U,10493
38
38
  signalwire_agents/core/swaig_function.py,sha256=KnUQ2g99kDSzOzD1PJ0Iqs8DeeZ6jDIIN54C5MA4TWw,7521
39
39
  signalwire_agents/core/swml_builder.py,sha256=tJBFDAVTENEfjGLp2h9_AKOYt5O9FrSYLI-nZZVwM1E,15604
@@ -70,13 +70,13 @@ signalwire_agents/prefabs/info_gatherer.py,sha256=0LpYTaU7C76Efp3yUIdNX6xzWH7mj5
70
70
  signalwire_agents/prefabs/receptionist.py,sha256=em0uk_F0tmePvzE6Hi9HFlL3MHChH0RaHHqSvww9pK0,10323
71
71
  signalwire_agents/prefabs/survey.py,sha256=a-0-xAnQYhdX4Lzgyna14lpNfaCV-rLUFkQF6QOCQAY,14534
72
72
  signalwire_agents/search/__init__.py,sha256=cb8Rtg4Jut9ZhuSbiaHl79G0iWgMhJkLu84urkY0lRc,4215
73
- signalwire_agents/search/document_processor.py,sha256=zDih-xBWq2kEQkQblh4xXZE1uIQ9aBBFLjjsi3CYfRU,48196
73
+ signalwire_agents/search/document_processor.py,sha256=emlmYAgr34Qyhfqm4VppqTCpm1cj8Db6ziHoFuw4ekE,51929
74
74
  signalwire_agents/search/index_builder.py,sha256=v1LGhbzzKlCilO4g6nqQJVYEAWvInP2j5B1QrAEj4V8,33772
75
75
  signalwire_agents/search/migration.py,sha256=UZPrpUOMZeLVNO1cEDp3tnZYG6ys8-VCFlZXmzig_E0,16582
76
76
  signalwire_agents/search/models.py,sha256=isYOYwQT0eWCVdcYSSd8w6z2gFYUobtC8BAUhV7FUVI,840
77
77
  signalwire_agents/search/pgvector_backend.py,sha256=waneT_cBmWGE79kpN5Ie4ax6VDuyWb7QmjzXzGFjY7w,28400
78
78
  signalwire_agents/search/query_processor.py,sha256=qpUFQqxobx8IymcXBTdPUirfawZVCKtRdSlMHw3nA0s,19656
79
- signalwire_agents/search/search_engine.py,sha256=dHwhAknIw0paAVQx3ccgVOkaiFqMpDOAriH8lQrBYm8,55671
79
+ signalwire_agents/search/search_engine.py,sha256=SRUNXJlDBej_kK4JpsksY0bW-pDdnY6Cdpbu648ox9E,57445
80
80
  signalwire_agents/search/search_service.py,sha256=KV0luon18gPBcoyQ8L_Lagi1CgkJSOwGWshpCjvj1ks,20360
81
81
  signalwire_agents/skills/README.md,sha256=sM1_08IsKdRDCzYHPLzppJbaK5MvRelsVL6Kd9A9Ubo,12193
82
82
  signalwire_agents/skills/__init__.py,sha256=9AMEcyk2tDaGiUjwVIson_tVWxV4oU_2NnGGNTbHuyQ,533
@@ -86,31 +86,31 @@ signalwire_agents/skills/api_ninjas_trivia/__init__.py,sha256=zN305bBQkzlJyUNsPU
86
86
  signalwire_agents/skills/api_ninjas_trivia/skill.py,sha256=ajJm0Vd07Oz3h0sHP0rRyckAXAbFRtcP7ws9GiAhfjw,8626
87
87
  signalwire_agents/skills/datasphere/README.md,sha256=7G5t0V04SlnJ39U-3zOoIOfkNFrVEo-s45lCUlYmJGo,7351
88
88
  signalwire_agents/skills/datasphere/__init__.py,sha256=SJJlmeMSeezjINPgkuWN1XzDPN_Z3GzZ_StzO1BtxQs,257
89
- signalwire_agents/skills/datasphere/skill.py,sha256=EQ7ODzTt591wBlTDTn4ogX8LTeygiU8YHFQTx3F0f2c,12682
89
+ signalwire_agents/skills/datasphere/skill.py,sha256=OcDgmFSvIA3qy6a24E_fIgXrPKGRnLRHtPh9JeJ5YOY,12643
90
90
  signalwire_agents/skills/datasphere_serverless/README.md,sha256=FErV97NEdYD_N1wZxkLqy6DSml5B9mCJmEgCUdGxh6A,9299
91
91
  signalwire_agents/skills/datasphere_serverless/__init__.py,sha256=jpMNDcGiXsVbSCVUrc_AwLARqEtVu4dPYZPJSJ-K3rc,261
92
92
  signalwire_agents/skills/datasphere_serverless/skill.py,sha256=i57VkMo2gU5YE9Z2lIxFfZtYkvkwMpnek49eSSfMFS0,9882
93
93
  signalwire_agents/skills/datetime/README.md,sha256=95SzVz-Pcm9MPqZ4D3sSYKMwdpsDNwwCpWFRK027-Pc,4534
94
94
  signalwire_agents/skills/datetime/__init__.py,sha256=Irajm2sUhmQVFgais-J-q-3d58tNnJ4nbLmnphr90nI,234
95
- signalwire_agents/skills/datetime/skill.py,sha256=mA-dxBhZOIbMygBQ3Z4jmFCH-zsD8HnjcWC4asp6Gl0,4370
95
+ signalwire_agents/skills/datetime/skill.py,sha256=-MnycSmhK2JWwuHAStxz0mHNuMIvvWCW-I9La44WDts,4292
96
96
  signalwire_agents/skills/joke/README.md,sha256=xUa2_0Pk9neli-UJxI4BPt3Fb1_5Xa3m8RuDlrkfBao,3594
97
97
  signalwire_agents/skills/joke/__init__.py,sha256=8Rc5_nj30bdga2n9H9JSI2WzMn40pjApd-y-tk5WIkI,244
98
98
  signalwire_agents/skills/joke/skill.py,sha256=BKPA50iht8I_mVBJ-PIQHjJJz1m0V2w5J69AfVMx23o,4092
99
99
  signalwire_agents/skills/math/README.md,sha256=Nrv7PxkFPSxdnAN6856Fp1CfvsUwdncpRFFDERxmMe0,5335
100
100
  signalwire_agents/skills/math/__init__.py,sha256=F7emZqBpAAkqJZxA3RNTzRSAXE5e2xu8PtFOPHebfKo,230
101
- signalwire_agents/skills/math/skill.py,sha256=-h0DRX_nFkeSzLfaiOKv0zHScdXiQuz4rkLShanKwmc,3800
101
+ signalwire_agents/skills/math/skill.py,sha256=pfwlljnN_UoT9rzkggzPrgGWuoZ_wnovrph-aLG4ULY,3761
102
102
  signalwire_agents/skills/mcp_gateway/README.md,sha256=t-71TTWlEvjgWLTcT3v4kMw9zlrKXTAC_sCjb1haNew,5826
103
103
  signalwire_agents/skills/mcp_gateway/__init__.py,sha256=zLgOa7s0sIQphTNJjvasIAW7llxAApez7moC_e1tzP0,236
104
- signalwire_agents/skills/mcp_gateway/skill.py,sha256=rtXs8CayjWH8WOrpjGMbbG11dJCNK2RUA06Ysc1cK8g,17167
104
+ signalwire_agents/skills/mcp_gateway/skill.py,sha256=bOdfBFsboRWSVoRA5X9vcmkH9OJIQxUllhMzhqHV1XI,17155
105
105
  signalwire_agents/skills/native_vector_search/README.md,sha256=eFVRoDwZlZwbBXUKyKrvfC6AL4T8MXj0B-IgIdBZF70,5526
106
106
  signalwire_agents/skills/native_vector_search/__init__.py,sha256=RofpN3Sd-vyWeUCTYH2dRVrl7h6YuyG5OK772UQ-KFk,220
107
- signalwire_agents/skills/native_vector_search/skill.py,sha256=ltIFXwxxyqYkTSG2exUsSuJB4tOAiTwYrG7PYtVDCJw,36208
107
+ signalwire_agents/skills/native_vector_search/skill.py,sha256=SwJ3fkFbAdW-zkbhTkzdj_0VJGXm-j2bRxE89CLu4B8,37403
108
108
  signalwire_agents/skills/play_background_file/README.md,sha256=omJ_jY5Co6Mk-gJt_hoSl40wemmTbzae3DBll6HL0B4,7026
109
109
  signalwire_agents/skills/play_background_file/__init__.py,sha256=iETc6e-0Cai3RUTQWhg9BieWi3NF3_DWWBKdYXcd4ok,273
110
110
  signalwire_agents/skills/play_background_file/skill.py,sha256=HgPc2FIvXKJHZ7gO2QEzQe6-uUBPrw_6sRJJpU83GTY,8822
111
111
  signalwire_agents/skills/spider/README.md,sha256=yBa09JzgLikG3STbDNbRCKUM3l3XU5-D923I2g8CTVc,6909
112
112
  signalwire_agents/skills/spider/__init__.py,sha256=bZcCGLX5Cz18qY8rOvAAync6BRtketxaU19l6YcA_iI,285
113
- signalwire_agents/skills/spider/skill.py,sha256=w4dj0M8RZSfJtzL-jPXvr_roqawwnFKQD5GDKreel1Y,23137
113
+ signalwire_agents/skills/spider/skill.py,sha256=_gdw_fJO8wZhs1yx-JGdY8Tf-605NjOG-KH44-Js9Cc,23020
114
114
  signalwire_agents/skills/swml_transfer/README.md,sha256=2Y6CH5Bm9kI5IYCLczIQIYlaYUq6VX_S4Irct2CQMmQ,14681
115
115
  signalwire_agents/skills/swml_transfer/__init__.py,sha256=YyfxRpbgT4ZpEjGolwffKqjUzX4VqDNLdqfSoA0D0IY,238
116
116
  signalwire_agents/skills/swml_transfer/skill.py,sha256=_qzJRd9P5VN8flTDe9N-9cvsLU0sN7XuY5yjk-DNlv8,15363
@@ -119,10 +119,10 @@ signalwire_agents/skills/weather_api/__init__.py,sha256=WCS--GFBX8straIZPuGAmTDZ
119
119
  signalwire_agents/skills/weather_api/skill.py,sha256=LNJItYzgRSZYNYcH7Z37BOjjPy3aaM0OjMRnAxiUhOI,7204
120
120
  signalwire_agents/skills/web_search/README.md,sha256=Y95cxEScMzhmslUJF8u_Nh15FbEBuus4P-E8_kk2an0,5438
121
121
  signalwire_agents/skills/web_search/__init__.py,sha256=kv4CzmF1lldRZcL_HivieslP7gtTFvxcfprKG4n6b-Q,236
122
- signalwire_agents/skills/web_search/skill.py,sha256=EGu6ff9aAb2W323_XDCcVDW1wbAKTZYK8HQOT__iqtE,12660
122
+ signalwire_agents/skills/web_search/skill.py,sha256=h1zrAUFhC5Ul2BaKQY4Fn05Pob8AG-9eaSwE3ko-g3Y,13459
123
123
  signalwire_agents/skills/wikipedia_search/README.md,sha256=KFIQ8XhqrTG8NRs72dIbjJacy2DlYEXLtxgy23gyRi4,7585
124
124
  signalwire_agents/skills/wikipedia_search/__init__.py,sha256=yJ6iYTSyJC96mwwUsI_FneFhDBcLYD4xEerBKlWLTb8,375
125
- signalwire_agents/skills/wikipedia_search/skill.py,sha256=4sirBiEXn0Sd1YOnyinUFjACFq0y-CtzbdnDGnGzOAA,7949
125
+ signalwire_agents/skills/wikipedia_search/skill.py,sha256=peNxT3GHMJY5OFQ-weneO83NpSstGjcmrTN_ENzUsEo,7910
126
126
  signalwire_agents/utils/__init__.py,sha256=1KVsHzwgfktSXHe3vqSRGImjtIE58szwD2FHHoFBtvY,601
127
127
  signalwire_agents/utils/pom_utils.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
128
128
  signalwire_agents/utils/schema_utils.py,sha256=i4okv_O9bUApwT_jJf4Yoij3bLCrGrW3DC-vzSy2RuY,16392
@@ -130,9 +130,9 @@ signalwire_agents/utils/token_generators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663
130
130
  signalwire_agents/utils/validators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
131
131
  signalwire_agents/web/__init__.py,sha256=XE_pSTY9Aalzr7J7wqFth1Zr3cccQHPPcF5HWNrOpz8,383
132
132
  signalwire_agents/web/web_service.py,sha256=a2PSHJgX1tlZr0Iz1A1UouZjXEePJAZL632evvLVM38,21071
133
- signalwire_agents-0.1.50.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
134
- signalwire_agents-0.1.50.dist-info/METADATA,sha256=IKAyYdw0b9B6ZATtnnqiwOaloF2LbGGcpoZ0suslxME,41596
135
- signalwire_agents-0.1.50.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
136
- signalwire_agents-0.1.50.dist-info/entry_points.txt,sha256=ZDT65zfTO_YyDzi_hwQbCxIhrUfu_t8RpNXMMXlUPWI,144
137
- signalwire_agents-0.1.50.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
138
- signalwire_agents-0.1.50.dist-info/RECORD,,
133
+ signalwire_agents-0.1.53.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
134
+ signalwire_agents-0.1.53.dist-info/METADATA,sha256=_6Ucazw9iFa0MnkICONuv0ckR6_5lkFMcUq5-yg46RY,41596
135
+ signalwire_agents-0.1.53.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
136
+ signalwire_agents-0.1.53.dist-info/entry_points.txt,sha256=ZDT65zfTO_YyDzi_hwQbCxIhrUfu_t8RpNXMMXlUPWI,144
137
+ signalwire_agents-0.1.53.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
138
+ signalwire_agents-0.1.53.dist-info/RECORD,,