signalwire-agents 0.1.16__py3-none-any.whl → 0.1.18__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.16"
21
+ __version__ = "0.1.18"
22
22
 
23
23
  # Import core classes for easier access
24
24
  from .core.agent_base import AgentBase
@@ -85,6 +85,10 @@ Examples:
85
85
  sw-search search ./docs.swsearch "how to create an agent"
86
86
  sw-search search ./docs.swsearch "API reference" --count 3 --verbose
87
87
  sw-search search ./docs.swsearch "configuration" --tags documentation --json
88
+
89
+ # Search via remote API
90
+ sw-search remote http://localhost:8001 "how to create an agent" --index-name docs
91
+ sw-search remote localhost:8001 "API reference" --index-name docs --count 3 --verbose
88
92
  """
89
93
  )
90
94
 
@@ -263,7 +267,7 @@ Examples:
263
267
 
264
268
  try:
265
269
  # Create index builder - import only when actually needed
266
- from ..search.index_builder import IndexBuilder
270
+ from signalwire_agents.search.index_builder import IndexBuilder
267
271
  builder = IndexBuilder(
268
272
  model_name=args.model,
269
273
  chunking_strategy=args.chunking_strategy,
@@ -328,7 +332,7 @@ def validate_command():
328
332
  sys.exit(1)
329
333
 
330
334
  try:
331
- from ..search.index_builder import IndexBuilder
335
+ from signalwire_agents.search.index_builder import IndexBuilder
332
336
  builder = IndexBuilder()
333
337
 
334
338
  validation = builder.validate_index(args.index_file)
@@ -376,8 +380,8 @@ def search_command():
376
380
  try:
377
381
  # Import search dependencies
378
382
  try:
379
- from ..search.search_engine import SearchEngine
380
- from ..search.query_processor import preprocess_query
383
+ from signalwire_agents.search.search_engine import SearchEngine
384
+ from signalwire_agents.search.query_processor import preprocess_query
381
385
  except ImportError as e:
382
386
  print(f"Error: Search functionality not available. Install with: pip install signalwire-agents[search]")
383
387
  print(f"Details: {e}")
@@ -478,6 +482,141 @@ def search_command():
478
482
  traceback.print_exc()
479
483
  sys.exit(1)
480
484
 
485
+ def remote_command():
486
+ """Search via remote API endpoint"""
487
+ parser = argparse.ArgumentParser(description='Search via remote API endpoint')
488
+ parser.add_argument('endpoint', help='Remote API endpoint URL (e.g., http://localhost:8001)')
489
+ parser.add_argument('query', help='Search query')
490
+ parser.add_argument('--index-name', required=True, help='Name of the index to search')
491
+ parser.add_argument('--count', type=int, default=5, help='Number of results to return (default: 5)')
492
+ parser.add_argument('--distance-threshold', type=float, default=0.0, help='Minimum similarity score (default: 0.0)')
493
+ parser.add_argument('--tags', help='Comma-separated tags to filter by')
494
+ parser.add_argument('--verbose', action='store_true', help='Show detailed information')
495
+ parser.add_argument('--json', action='store_true', help='Output results as JSON')
496
+ parser.add_argument('--no-content', action='store_true', help='Hide content in results (show only metadata)')
497
+ parser.add_argument('--timeout', type=int, default=30, help='Request timeout in seconds (default: 30)')
498
+
499
+ args = parser.parse_args()
500
+
501
+ # Ensure endpoint starts with http:// or https://
502
+ endpoint = args.endpoint
503
+ if not endpoint.startswith(('http://', 'https://')):
504
+ endpoint = f"http://{endpoint}"
505
+
506
+ # Ensure endpoint ends with /search
507
+ if not endpoint.endswith('/search'):
508
+ if endpoint.endswith('/'):
509
+ endpoint += 'search'
510
+ else:
511
+ endpoint += '/search'
512
+
513
+ try:
514
+ import requests
515
+ except ImportError:
516
+ print("Error: requests library not available. Install with: pip install requests")
517
+ sys.exit(1)
518
+
519
+ # Prepare request payload
520
+ payload = {
521
+ 'query': args.query,
522
+ 'index_name': args.index_name,
523
+ 'count': args.count,
524
+ 'distance_threshold': args.distance_threshold
525
+ }
526
+
527
+ if args.tags:
528
+ payload['tags'] = [tag.strip() for tag in args.tags.split(',')]
529
+
530
+ if args.verbose:
531
+ print(f"Searching remote endpoint: {endpoint}")
532
+ print(f"Payload: {payload}")
533
+ print()
534
+
535
+ try:
536
+ # Make the API request
537
+ response = requests.post(
538
+ endpoint,
539
+ json=payload,
540
+ headers={'Content-Type': 'application/json'},
541
+ timeout=args.timeout
542
+ )
543
+
544
+ if response.status_code == 200:
545
+ result = response.json()
546
+
547
+ if args.json:
548
+ # Output raw JSON response
549
+ import json
550
+ print(json.dumps(result, indent=2))
551
+ else:
552
+ # Human-readable output
553
+ results = result.get('results', [])
554
+ if not results:
555
+ print(f"No results found for '{args.query}' in index '{args.index_name}'")
556
+ sys.exit(0)
557
+
558
+ print(f"Found {len(results)} result(s) for '{args.query}' in index '{args.index_name}':")
559
+ if result.get('enhanced_query') and result.get('enhanced_query') != args.query:
560
+ print(f"Enhanced query: '{result.get('enhanced_query')}'")
561
+ print("=" * 80)
562
+
563
+ for i, search_result in enumerate(results):
564
+ print(f"\n[{i+1}] Score: {search_result.get('score', 0):.4f}")
565
+
566
+ # Show metadata
567
+ metadata = search_result.get('metadata', {})
568
+ print(f"File: {metadata.get('filename', 'Unknown')}")
569
+ if metadata.get('section'):
570
+ print(f"Section: {metadata['section']}")
571
+ if metadata.get('line_start'):
572
+ print(f"Lines: {metadata['line_start']}-{metadata.get('line_end', metadata['line_start'])}")
573
+ if metadata.get('tags'):
574
+ print(f"Tags: {', '.join(metadata['tags'])}")
575
+
576
+ # Show content unless suppressed
577
+ if not args.no_content and 'content' in search_result:
578
+ content = search_result['content']
579
+ if len(content) > 500 and not args.verbose:
580
+ content = content[:500] + "..."
581
+ print(f"\nContent:\n{content}")
582
+
583
+ if i < len(results) - 1:
584
+ print("-" * 80)
585
+
586
+ elif response.status_code == 404:
587
+ try:
588
+ error_detail = response.json()
589
+ error_msg = error_detail.get('detail', 'Index not found')
590
+ except:
591
+ error_msg = 'Index not found'
592
+ print(f"Error: {error_msg}")
593
+ sys.exit(1)
594
+ else:
595
+ try:
596
+ error_detail = response.json()
597
+ error_msg = error_detail.get('detail', f'HTTP {response.status_code}')
598
+ except:
599
+ error_msg = f'HTTP {response.status_code}: {response.text}'
600
+ print(f"Error: {error_msg}")
601
+ sys.exit(1)
602
+
603
+ except requests.ConnectionError:
604
+ print(f"Error: Could not connect to {endpoint}")
605
+ print("Make sure the search server is running")
606
+ sys.exit(1)
607
+ except requests.Timeout:
608
+ print(f"Error: Request timed out after {args.timeout} seconds")
609
+ sys.exit(1)
610
+ except requests.RequestException as e:
611
+ print(f"Error making request: {e}")
612
+ sys.exit(1)
613
+ except Exception as e:
614
+ print(f"Error: {e}")
615
+ if args.verbose:
616
+ import traceback
617
+ traceback.print_exc()
618
+ sys.exit(1)
619
+
481
620
  def console_entry_point():
482
621
  """Console script entry point for pip installation"""
483
622
  import sys
@@ -594,6 +733,10 @@ Examples:
594
733
  sw-search search ./docs.swsearch "how to create an agent"
595
734
  sw-search search ./docs.swsearch "API reference" --count 3 --verbose
596
735
  sw-search search ./docs.swsearch "configuration" --tags documentation --json
736
+
737
+ # Search via remote API
738
+ sw-search remote http://localhost:8001 "how to create an agent" --index-name docs
739
+ sw-search remote localhost:8001 "API reference" --index-name docs --count 3 --verbose
597
740
  """)
598
741
  return
599
742
 
@@ -609,6 +752,11 @@ Examples:
609
752
  sys.argv.pop(1)
610
753
  search_command()
611
754
  return
755
+ elif sys.argv[1] == 'remote':
756
+ # Remove 'remote' from argv and call remote_command
757
+ sys.argv.pop(1)
758
+ remote_command()
759
+ return
612
760
 
613
761
  # Regular build command
614
762
  main()
@@ -1496,6 +1496,10 @@ class AgentBase(SWMLService):
1496
1496
  if self._function_includes:
1497
1497
  swaig_obj["includes"] = self._function_includes
1498
1498
 
1499
+ # Add internal_fillers if any are defined
1500
+ if hasattr(self, '_internal_fillers') and self._internal_fillers:
1501
+ swaig_obj["internal_fillers"] = self._internal_fillers
1502
+
1499
1503
  # Create functions array
1500
1504
  functions = []
1501
1505
 
@@ -1577,13 +1581,25 @@ class AgentBase(SWMLService):
1577
1581
  try:
1578
1582
  # Check if we're in contexts mode
1579
1583
  if self._contexts_defined and self._contexts_builder:
1580
- # Generate contexts instead of prompt
1584
+ # Generate contexts and combine with base prompt
1581
1585
  contexts_dict = self._contexts_builder.to_dict()
1582
1586
 
1583
- # Build AI config with contexts
1587
+ # Determine base prompt (required when using contexts)
1588
+ base_prompt_text = None
1589
+ base_prompt_pom = None
1590
+
1591
+ if prompt_is_pom:
1592
+ base_prompt_pom = prompt
1593
+ elif prompt:
1594
+ base_prompt_text = prompt
1595
+ else:
1596
+ # Provide default base prompt if none exists
1597
+ base_prompt_text = f"You are {self.name}, a helpful AI assistant that follows structured workflows."
1598
+
1599
+ # Build AI config with base prompt + contexts
1584
1600
  ai_config = ai_handler.build_config(
1585
- prompt_text=None,
1586
- prompt_pom=None,
1601
+ prompt_text=base_prompt_text,
1602
+ prompt_pom=base_prompt_pom,
1587
1603
  contexts=contexts_dict,
1588
1604
  post_prompt=post_prompt,
1589
1605
  post_prompt_url=post_prompt_url,
@@ -2613,6 +2629,63 @@ class AgentBase(SWMLService):
2613
2629
  self.native_functions = [name for name in function_names if isinstance(name, str)]
2614
2630
  return self
2615
2631
 
2632
+ def set_internal_fillers(self, internal_fillers: Dict[str, Dict[str, List[str]]]) -> 'AgentBase':
2633
+ """
2634
+ Set internal fillers for native SWAIG functions
2635
+
2636
+ Internal fillers provide custom phrases the AI says while executing
2637
+ internal/native functions like check_time, wait_for_user, next_step, etc.
2638
+
2639
+ Args:
2640
+ internal_fillers: Dictionary mapping function names to language-specific filler phrases
2641
+ Format: {"function_name": {"language_code": ["phrase1", "phrase2"]}}
2642
+ Example: {"next_step": {"en-US": ["Moving to the next step...", "Great, let's continue..."]}}
2643
+
2644
+ Returns:
2645
+ Self for method chaining
2646
+
2647
+ Example:
2648
+ agent.set_internal_fillers({
2649
+ "next_step": {
2650
+ "en-US": ["Moving to the next step...", "Great, let's continue..."],
2651
+ "es": ["Pasando al siguiente paso...", "Excelente, continuemos..."]
2652
+ },
2653
+ "check_time": {
2654
+ "en-US": ["Let me check the time...", "Getting the current time..."]
2655
+ }
2656
+ })
2657
+ """
2658
+ if internal_fillers and isinstance(internal_fillers, dict):
2659
+ if not hasattr(self, '_internal_fillers'):
2660
+ self._internal_fillers = {}
2661
+ self._internal_fillers.update(internal_fillers)
2662
+ return self
2663
+
2664
+ def add_internal_filler(self, function_name: str, language_code: str, fillers: List[str]) -> 'AgentBase':
2665
+ """
2666
+ Add internal fillers for a specific function and language
2667
+
2668
+ Args:
2669
+ function_name: Name of the internal function (e.g., 'next_step', 'check_time')
2670
+ language_code: Language code (e.g., 'en-US', 'es', 'fr')
2671
+ fillers: List of filler phrases for this function and language
2672
+
2673
+ Returns:
2674
+ Self for method chaining
2675
+
2676
+ Example:
2677
+ agent.add_internal_filler("next_step", "en-US", ["Moving to the next step...", "Great, let's continue..."])
2678
+ """
2679
+ if function_name and language_code and fillers and isinstance(fillers, list):
2680
+ if not hasattr(self, '_internal_fillers'):
2681
+ self._internal_fillers = {}
2682
+
2683
+ if function_name not in self._internal_fillers:
2684
+ self._internal_fillers[function_name] = {}
2685
+
2686
+ self._internal_fillers[function_name][language_code] = fillers
2687
+ return self
2688
+
2616
2689
  def add_function_include(self, url: str, functions: List[str], meta_data: Optional[Dict[str, Any]] = None) -> 'AgentBase':
2617
2690
  """
2618
2691
  Add a remote function include to the SWAIG configuration
@@ -18,9 +18,16 @@ class Step:
18
18
  self._step_criteria: Optional[str] = None
19
19
  self._functions: Optional[Union[str, List[str]]] = None
20
20
  self._valid_steps: Optional[List[str]] = None
21
+ self._valid_contexts: Optional[List[str]] = None
21
22
 
22
23
  # POM-style sections for rich prompts
23
24
  self._sections: List[Dict[str, Any]] = []
25
+
26
+ # Reset object for context switching from steps
27
+ self._reset_system_prompt: Optional[str] = None
28
+ self._reset_user_prompt: Optional[str] = None
29
+ self._reset_consolidate: bool = False
30
+ self._reset_full_reset: bool = False
24
31
 
25
32
  def set_text(self, text: str) -> 'Step':
26
33
  """
@@ -108,6 +115,71 @@ class Step:
108
115
  self._valid_steps = steps
109
116
  return self
110
117
 
118
+ def set_valid_contexts(self, contexts: List[str]) -> 'Step':
119
+ """
120
+ Set which contexts can be navigated to from this step
121
+
122
+ Args:
123
+ contexts: List of valid context names
124
+
125
+ Returns:
126
+ Self for method chaining
127
+ """
128
+ self._valid_contexts = contexts
129
+ return self
130
+
131
+ def set_reset_system_prompt(self, system_prompt: str) -> 'Step':
132
+ """
133
+ Set system prompt for context switching when this step navigates to a context
134
+
135
+ Args:
136
+ system_prompt: New system prompt for context switching
137
+
138
+ Returns:
139
+ Self for method chaining
140
+ """
141
+ self._reset_system_prompt = system_prompt
142
+ return self
143
+
144
+ def set_reset_user_prompt(self, user_prompt: str) -> 'Step':
145
+ """
146
+ Set user prompt for context switching when this step navigates to a context
147
+
148
+ Args:
149
+ user_prompt: User message to inject for context switching
150
+
151
+ Returns:
152
+ Self for method chaining
153
+ """
154
+ self._reset_user_prompt = user_prompt
155
+ return self
156
+
157
+ def set_reset_consolidate(self, consolidate: bool) -> 'Step':
158
+ """
159
+ Set whether to consolidate conversation when this step switches contexts
160
+
161
+ Args:
162
+ consolidate: Whether to consolidate previous conversation
163
+
164
+ Returns:
165
+ Self for method chaining
166
+ """
167
+ self._reset_consolidate = consolidate
168
+ return self
169
+
170
+ def set_reset_full_reset(self, full_reset: bool) -> 'Step':
171
+ """
172
+ Set whether to do full reset when this step switches contexts
173
+
174
+ Args:
175
+ full_reset: Whether to completely rewrite system prompt vs inject
176
+
177
+ Returns:
178
+ Self for method chaining
179
+ """
180
+ self._reset_full_reset = full_reset
181
+ return self
182
+
111
183
  def _render_text(self) -> str:
112
184
  """Render the step's prompt text"""
113
185
  if self._text is not None:
@@ -145,6 +217,23 @@ class Step:
145
217
  if self._valid_steps is not None:
146
218
  step_dict["valid_steps"] = self._valid_steps
147
219
 
220
+ if self._valid_contexts is not None:
221
+ step_dict["valid_contexts"] = self._valid_contexts
222
+
223
+ # Add reset object if any reset parameters are set
224
+ reset_obj = {}
225
+ if self._reset_system_prompt is not None:
226
+ reset_obj["system_prompt"] = self._reset_system_prompt
227
+ if self._reset_user_prompt is not None:
228
+ reset_obj["user_prompt"] = self._reset_user_prompt
229
+ if self._reset_consolidate:
230
+ reset_obj["consolidate"] = self._reset_consolidate
231
+ if self._reset_full_reset:
232
+ reset_obj["full_reset"] = self._reset_full_reset
233
+
234
+ if reset_obj:
235
+ step_dict["reset"] = reset_obj
236
+
148
237
  return step_dict
149
238
 
150
239
 
@@ -156,6 +245,19 @@ class Context:
156
245
  self._steps: Dict[str, Step] = {}
157
246
  self._step_order: List[str] = []
158
247
  self._valid_contexts: Optional[List[str]] = None
248
+
249
+ # Context entry parameters
250
+ self._post_prompt: Optional[str] = None
251
+ self._system_prompt: Optional[str] = None
252
+ self._system_prompt_sections: List[Dict[str, Any]] = [] # For POM-style system prompts
253
+ self._consolidate: bool = False
254
+ self._full_reset: bool = False
255
+ self._user_prompt: Optional[str] = None
256
+ self._isolated: bool = False
257
+
258
+ # Context prompt (separate from system_prompt)
259
+ self._prompt_text: Optional[str] = None
260
+ self._prompt_sections: List[Dict[str, Any]] = []
159
261
 
160
262
  def add_step(self, name: str) -> Step:
161
263
  """
@@ -188,6 +290,209 @@ class Context:
188
290
  self._valid_contexts = contexts
189
291
  return self
190
292
 
293
+ def set_post_prompt(self, post_prompt: str) -> 'Context':
294
+ """
295
+ Set post prompt override for this context
296
+
297
+ Args:
298
+ post_prompt: Post prompt text to use when this context is active
299
+
300
+ Returns:
301
+ Self for method chaining
302
+ """
303
+ self._post_prompt = post_prompt
304
+ return self
305
+
306
+ def set_system_prompt(self, system_prompt: str) -> 'Context':
307
+ """
308
+ Set system prompt for context switching (triggers context reset)
309
+
310
+ Args:
311
+ system_prompt: New system prompt for when this context is entered
312
+
313
+ Returns:
314
+ Self for method chaining
315
+ """
316
+ if self._system_prompt_sections:
317
+ raise ValueError("Cannot use set_system_prompt() when POM sections have been added for system prompt. Use one approach or the other.")
318
+ self._system_prompt = system_prompt
319
+ return self
320
+
321
+ def set_consolidate(self, consolidate: bool) -> 'Context':
322
+ """
323
+ Set whether to consolidate conversation history when entering this context
324
+
325
+ Args:
326
+ consolidate: Whether to consolidate previous conversation
327
+
328
+ Returns:
329
+ Self for method chaining
330
+ """
331
+ self._consolidate = consolidate
332
+ return self
333
+
334
+ def set_full_reset(self, full_reset: bool) -> 'Context':
335
+ """
336
+ Set whether to do full reset when entering this context
337
+
338
+ Args:
339
+ full_reset: Whether to completely rewrite system prompt vs inject
340
+
341
+ Returns:
342
+ Self for method chaining
343
+ """
344
+ self._full_reset = full_reset
345
+ return self
346
+
347
+ def set_user_prompt(self, user_prompt: str) -> 'Context':
348
+ """
349
+ Set user prompt to inject when entering this context
350
+
351
+ Args:
352
+ user_prompt: User message to inject for context
353
+
354
+ Returns:
355
+ Self for method chaining
356
+ """
357
+ self._user_prompt = user_prompt
358
+ return self
359
+
360
+ def set_isolated(self, isolated: bool) -> 'Context':
361
+ """
362
+ Set whether to truncate conversation history when entering this context
363
+
364
+ Args:
365
+ isolated: Whether to truncate conversation on context switch
366
+
367
+ Returns:
368
+ Self for method chaining
369
+ """
370
+ self._isolated = isolated
371
+ return self
372
+
373
+ def add_system_section(self, title: str, body: str) -> 'Context':
374
+ """
375
+ Add a POM section to the system prompt
376
+
377
+ Args:
378
+ title: Section title
379
+ body: Section body text
380
+
381
+ Returns:
382
+ Self for method chaining
383
+ """
384
+ if self._system_prompt is not None:
385
+ raise ValueError("Cannot add POM sections for system prompt when set_system_prompt() has been used. Use one approach or the other.")
386
+ self._system_prompt_sections.append({"title": title, "body": body})
387
+ return self
388
+
389
+ def add_system_bullets(self, title: str, bullets: List[str]) -> 'Context':
390
+ """
391
+ Add a POM section with bullet points to the system prompt
392
+
393
+ Args:
394
+ title: Section title
395
+ bullets: List of bullet points
396
+
397
+ Returns:
398
+ Self for method chaining
399
+ """
400
+ if self._system_prompt is not None:
401
+ raise ValueError("Cannot add POM sections for system prompt when set_system_prompt() has been used. Use one approach or the other.")
402
+ self._system_prompt_sections.append({"title": title, "bullets": bullets})
403
+ return self
404
+
405
+ def set_prompt(self, prompt: str) -> 'Context':
406
+ """
407
+ Set the context's prompt text directly
408
+
409
+ Args:
410
+ prompt: The prompt text for this context
411
+
412
+ Returns:
413
+ Self for method chaining
414
+ """
415
+ if self._prompt_sections:
416
+ raise ValueError("Cannot use set_prompt() when POM sections have been added. Use one approach or the other.")
417
+ self._prompt_text = prompt
418
+ return self
419
+
420
+ def add_section(self, title: str, body: str) -> 'Context':
421
+ """
422
+ Add a POM section to the context prompt
423
+
424
+ Args:
425
+ title: Section title
426
+ body: Section body text
427
+
428
+ Returns:
429
+ Self for method chaining
430
+ """
431
+ if self._prompt_text is not None:
432
+ raise ValueError("Cannot add POM sections when set_prompt() has been used. Use one approach or the other.")
433
+ self._prompt_sections.append({"title": title, "body": body})
434
+ return self
435
+
436
+ def add_bullets(self, title: str, bullets: List[str]) -> 'Context':
437
+ """
438
+ Add a POM section with bullet points to the context prompt
439
+
440
+ Args:
441
+ title: Section title
442
+ bullets: List of bullet points
443
+
444
+ Returns:
445
+ Self for method chaining
446
+ """
447
+ if self._prompt_text is not None:
448
+ raise ValueError("Cannot add POM sections when set_prompt() has been used. Use one approach or the other.")
449
+ self._prompt_sections.append({"title": title, "bullets": bullets})
450
+ return self
451
+
452
+ def _render_prompt(self) -> Optional[str]:
453
+ """Render the context's prompt text"""
454
+ if self._prompt_text is not None:
455
+ return self._prompt_text
456
+
457
+ if not self._prompt_sections:
458
+ return None
459
+
460
+ # Convert POM sections to markdown
461
+ markdown_parts = []
462
+ for section in self._prompt_sections:
463
+ if "bullets" in section:
464
+ markdown_parts.append(f"## {section['title']}")
465
+ for bullet in section["bullets"]:
466
+ markdown_parts.append(f"- {bullet}")
467
+ else:
468
+ markdown_parts.append(f"## {section['title']}")
469
+ markdown_parts.append(section["body"])
470
+ markdown_parts.append("") # Add spacing
471
+
472
+ return "\n".join(markdown_parts).strip()
473
+
474
+ def _render_system_prompt(self) -> Optional[str]:
475
+ """Render the system prompt text"""
476
+ if self._system_prompt is not None:
477
+ return self._system_prompt
478
+
479
+ if not self._system_prompt_sections:
480
+ return None
481
+
482
+ # Convert POM sections to markdown
483
+ markdown_parts = []
484
+ for section in self._system_prompt_sections:
485
+ if "bullets" in section:
486
+ markdown_parts.append(f"## {section['title']}")
487
+ for bullet in section["bullets"]:
488
+ markdown_parts.append(f"- {bullet}")
489
+ else:
490
+ markdown_parts.append(f"## {section['title']}")
491
+ markdown_parts.append(section["body"])
492
+ markdown_parts.append("") # Add spacing
493
+
494
+ return "\n".join(markdown_parts).strip()
495
+
191
496
  def to_dict(self) -> Dict[str, Any]:
192
497
  """Convert context to dictionary for SWML generation"""
193
498
  if not self._steps:
@@ -199,6 +504,34 @@ class Context:
199
504
 
200
505
  if self._valid_contexts is not None:
201
506
  context_dict["valid_contexts"] = self._valid_contexts
507
+
508
+ # Add context entry parameters
509
+ if self._post_prompt is not None:
510
+ context_dict["post_prompt"] = self._post_prompt
511
+
512
+ rendered_system_prompt = self._render_system_prompt()
513
+ if rendered_system_prompt is not None:
514
+ context_dict["system_prompt"] = rendered_system_prompt
515
+
516
+ if self._consolidate:
517
+ context_dict["consolidate"] = self._consolidate
518
+
519
+ if self._full_reset:
520
+ context_dict["full_reset"] = self._full_reset
521
+
522
+ if self._user_prompt is not None:
523
+ context_dict["user_prompt"] = self._user_prompt
524
+
525
+ if self._isolated:
526
+ context_dict["isolated"] = self._isolated
527
+
528
+ # Add context prompt - use POM structure if sections exist, otherwise use string
529
+ if self._prompt_sections:
530
+ # Use structured POM format
531
+ context_dict["pom"] = self._prompt_sections
532
+ elif self._prompt_text is not None:
533
+ # Use string format
534
+ context_dict["prompt"] = self._prompt_text
202
535
 
203
536
  return context_dict
204
537
 
@@ -103,19 +103,19 @@ class AIVerbHandler(SWMLVerbHandler):
103
103
  errors.append("'prompt' must be an object")
104
104
  return False, errors
105
105
 
106
- # Check that prompt contains one of: text, pom, or contexts
106
+ # Check that prompt contains either text or pom (required)
107
107
  has_text = "text" in prompt
108
108
  has_pom = "pom" in prompt
109
109
  has_contexts = "contexts" in prompt
110
110
 
111
- options_count = sum([has_text, has_pom, has_contexts])
111
+ # Require either text or pom (mutually exclusive)
112
+ base_prompt_count = sum([has_text, has_pom])
113
+ if base_prompt_count == 0:
114
+ errors.append("'prompt' must contain either 'text' or 'pom' as base prompt")
115
+ elif base_prompt_count > 1:
116
+ errors.append("'prompt' can only contain one of: 'text' or 'pom' (mutually exclusive)")
112
117
 
113
- if options_count == 0:
114
- errors.append("'prompt' must contain one of: 'text', 'pom', or 'contexts'")
115
- elif options_count > 1:
116
- errors.append("'prompt' can only contain one of: 'text', 'pom', or 'contexts'")
117
-
118
- # Validate contexts structure if present
118
+ # Contexts are optional and can be combined with text or pom
119
119
  if has_contexts:
120
120
  contexts = prompt["contexts"]
121
121
  if not isinstance(contexts, dict):
@@ -141,9 +141,9 @@ class AIVerbHandler(SWMLVerbHandler):
141
141
  Build a configuration for the AI verb
142
142
 
143
143
  Args:
144
- prompt_text: Text prompt for the AI (mutually exclusive with prompt_pom and contexts)
145
- prompt_pom: POM structure for the AI prompt (mutually exclusive with prompt_text and contexts)
146
- contexts: Contexts and steps configuration (mutually exclusive with prompt_text and prompt_pom)
144
+ prompt_text: Text prompt for the AI (mutually exclusive with prompt_pom)
145
+ prompt_pom: POM structure for the AI prompt (mutually exclusive with prompt_text)
146
+ contexts: Optional contexts and steps configuration (can be combined with text or pom)
147
147
  post_prompt: Optional post-prompt text
148
148
  post_prompt_url: Optional URL for post-prompt processing
149
149
  swaig: Optional SWAIG configuration
@@ -154,19 +154,25 @@ class AIVerbHandler(SWMLVerbHandler):
154
154
  """
155
155
  config = {}
156
156
 
157
- # Add prompt (either text, POM, or contexts - mutually exclusive)
158
- prompt_options_count = sum(x is not None for x in [prompt_text, prompt_pom, contexts])
159
- if prompt_options_count == 0:
160
- raise ValueError("One of prompt_text, prompt_pom, or contexts must be provided")
161
- elif prompt_options_count > 1:
162
- raise ValueError("prompt_text, prompt_pom, and contexts are mutually exclusive")
157
+ # Require either text or pom as base prompt (mutually exclusive)
158
+ base_prompt_count = sum(x is not None for x in [prompt_text, prompt_pom])
159
+ if base_prompt_count == 0:
160
+ raise ValueError("Either prompt_text or prompt_pom must be provided as base prompt")
161
+ elif base_prompt_count > 1:
162
+ raise ValueError("prompt_text and prompt_pom are mutually exclusive")
163
163
 
164
+ # Build prompt object with base prompt
165
+ prompt_config = {}
164
166
  if prompt_text is not None:
165
- config["prompt"] = {"text": prompt_text}
167
+ prompt_config["text"] = prompt_text
166
168
  elif prompt_pom is not None:
167
- config["prompt"] = {"pom": prompt_pom}
168
- elif contexts is not None:
169
- config["prompt"] = {"contexts": contexts}
169
+ prompt_config["pom"] = prompt_pom
170
+
171
+ # Add contexts if provided (optional, activates steps feature)
172
+ if contexts is not None:
173
+ prompt_config["contexts"] = contexts
174
+
175
+ config["prompt"] = prompt_config
170
176
 
171
177
  # Add post-prompt if provided
172
178
  if post_prompt is not None:
@@ -224,7 +224,7 @@ class WebSearchSkill(SkillBase):
224
224
  formatted_message = self.no_results_message.format(query=query) if '{query}' in self.no_results_message else self.no_results_message
225
225
  return SwaigFunctionResult(formatted_message)
226
226
 
227
- response = f"I found {num_results} results for '{query}':\n\n{search_results}"
227
+ response = f"Here are {num_results} results for '{query}':\n\nReiterate them to the user in a concise summary format\n\n{search_results}"
228
228
  return SwaigFunctionResult(response)
229
229
 
230
230
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalwire_agents
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: SignalWire AI Agents SDK
5
5
  Author-email: SignalWire Team <info@signalwire.com>
6
6
  Project-URL: Homepage, https://github.com/signalwire/signalwire-ai-agents
@@ -287,7 +287,7 @@ For detailed documentation, see [DataMap Guide](docs/datamap_guide.md).
287
287
 
288
288
  ## Contexts and Steps
289
289
 
290
- The SignalWire Agents SDK provides a powerful alternative to traditional Prompt Object Model (POM) prompts through the **Contexts and Steps** system. This feature allows you to create structured, workflow-driven AI interactions with explicit navigation control and step-by-step guidance.
290
+ The SignalWire Agents SDK provides a powerful enhancement to traditional prompts through the **Contexts and Steps** system. This feature allows you to add structured, workflow-driven AI interactions on top of your base prompt, with explicit navigation control and step-by-step guidance.
291
291
 
292
292
  ### Why Use Contexts and Steps?
293
293
 
@@ -296,7 +296,7 @@ The SignalWire Agents SDK provides a powerful alternative to traditional Prompt
296
296
  - **Completion Criteria**: Set specific criteria for step completion and progression
297
297
  - **Function Restrictions**: Limit which AI tools are available in each step
298
298
  - **Workflow Isolation**: Create separate contexts for different conversation flows
299
- - **Backward Compatibility**: Works alongside traditional prompts and all existing AgentBase features
299
+ - **Enhanced Base Prompts**: Adds structured workflows on top of your existing prompt foundation
300
300
 
301
301
  ### Basic Usage
302
302
 
@@ -307,30 +307,34 @@ class WorkflowAgent(AgentBase):
307
307
  def __init__(self):
308
308
  super().__init__(name="Workflow Assistant", route="/workflow")
309
309
 
310
- # Define contexts and steps (alternative to traditional prompts)
310
+ # Set base prompt (required even when using contexts)
311
+ self.prompt_add_section("Role", "You are a helpful workflow assistant.")
312
+ self.prompt_add_section("Instructions", "Guide users through structured processes step by step.")
313
+
314
+ # Define contexts and steps (adds structured workflow to base prompt)
311
315
  contexts = self.define_contexts()
312
316
 
313
317
  # Create a single context named "default" (required for single context)
314
- context = contexts.create_context("default")
318
+ context = contexts.add_context("default")
315
319
 
316
320
  # Add step-by-step workflow
317
- context.create_step("greeting") \
321
+ context.add_step("greeting") \
318
322
  .set_text("Welcome! I'm here to help you complete your application. Let's start with your personal information.") \
319
323
  .set_step_criteria("User has provided their name and confirmed they want to continue") \
320
324
  .set_valid_steps(["personal_info"]) # Can only go to personal_info step
321
325
 
322
- context.create_step("personal_info") \
326
+ context.add_step("personal_info") \
323
327
  .add_section("Instructions", "Collect the user's personal information") \
324
328
  .add_bullets(["Ask for full name", "Ask for email address", "Ask for phone number"]) \
325
329
  .set_step_criteria("All personal information has been collected and confirmed") \
326
330
  .set_valid_steps(["review", "personal_info"]) # Can stay or move to review
327
331
 
328
- context.create_step("review") \
332
+ context.add_step("review") \
329
333
  .set_text("Let me review the information you've provided. Please confirm if everything is correct.") \
330
334
  .set_step_criteria("User has confirmed or requested changes") \
331
335
  .set_valid_steps(["personal_info", "complete"]) # Can go back or complete
332
336
 
333
- context.create_step("complete") \
337
+ context.add_step("complete") \
334
338
  .set_text("Thank you! Your application has been submitted successfully.") \
335
339
  .set_step_criteria("Application processing is complete")
336
340
  # No valid_steps = end of workflow
@@ -346,22 +350,27 @@ class MultiContextAgent(AgentBase):
346
350
  def __init__(self):
347
351
  super().__init__(name="Multi-Context Agent", route="/multi-context")
348
352
 
349
- # Add skills first
353
+ # Set base prompt (required)
354
+ self.prompt_add_section("Role", "You are a versatile AI assistant.")
355
+ self.prompt_add_section("Capabilities", "You can help with calculations and provide time information.")
356
+
357
+ # Add skills
350
358
  self.add_skill("datetime")
351
359
  self.add_skill("math")
352
360
 
361
+ # Define contexts for different service modes
353
362
  contexts = self.define_contexts()
354
363
 
355
364
  # Main conversation context
356
- main_context = contexts.create_context("main")
357
- main_context.create_step("welcome") \
365
+ main_context = contexts.add_context("main")
366
+ main_context.add_step("welcome") \
358
367
  .set_text("Welcome! I can help with calculations or provide date/time info. What would you like to do?") \
359
368
  .set_step_criteria("User has chosen a service type") \
360
369
  .set_valid_contexts(["calculator", "datetime_info"]) # Can switch contexts
361
370
 
362
371
  # Calculator context with function restrictions
363
- calc_context = contexts.create_context("calculator")
364
- calc_context.create_step("math_mode") \
372
+ calc_context = contexts.add_context("calculator")
373
+ calc_context.add_step("math_mode") \
365
374
  .add_section("Role", "You are a mathematical assistant") \
366
375
  .add_section("Instructions", "Help users with calculations") \
367
376
  .set_functions(["math"]) # Only math function available \
@@ -369,8 +378,8 @@ class MultiContextAgent(AgentBase):
369
378
  .set_valid_contexts(["main"]) # Can return to main
370
379
 
371
380
  # DateTime context
372
- datetime_context = contexts.create_context("datetime_info")
373
- datetime_context.create_step("time_mode") \
381
+ datetime_context = contexts.add_context("datetime_info")
382
+ datetime_context.add_step("time_mode") \
374
383
  .set_text("I can provide current date and time information. What would you like to know?") \
375
384
  .set_functions(["datetime"]) # Only datetime function available \
376
385
  .set_step_criteria("Date/time information has been provided") \
@@ -380,7 +389,7 @@ class MultiContextAgent(AgentBase):
380
389
  ### Context and Step Methods
381
390
 
382
391
  #### Context Methods
383
- - `create_step(name)`: Create a new step in this context
392
+ - `add_step(name)`: Create a new step in this context
384
393
  - `set_valid_contexts(contexts)`: Control which contexts can be accessed from this context
385
394
 
386
395
  #### Step Methods
@@ -406,41 +415,45 @@ class SupportAgent(AgentBase):
406
415
  def __init__(self):
407
416
  super().__init__(name="Customer Support", route="/support")
408
417
 
418
+ # Set base prompt (required)
419
+ self.prompt_add_section("Role", "You are a professional customer support representative.")
420
+ self.prompt_add_section("Goal", "Provide excellent customer service using structured workflows.")
421
+
409
422
  # Add skills for enhanced capabilities
410
423
  self.add_skill("datetime")
411
424
  self.add_skill("web_search", {"api_key": "your-key", "search_engine_id": "your-id"})
412
425
 
426
+ # Define support workflow contexts
413
427
  contexts = self.define_contexts()
414
428
 
415
429
  # Triage context
416
- triage = contexts.create_context("triage")
417
- triage.create_step("initial_greeting") \
418
- .add_section("Role", "You are a helpful customer support agent") \
419
- .add_section("Goal", "Understand the customer's issue and route them appropriately") \
420
- .add_bullets(["Be empathetic and professional", "Ask clarifying questions", "Categorize the issue"]) \
430
+ triage = contexts.add_context("triage")
431
+ triage.add_step("initial_greeting") \
432
+ .add_section("Current Task", "Understand the customer's issue and route them appropriately") \
433
+ .add_bullets("Questions to Ask", ["What problem are you experiencing?", "How urgent is this issue?", "Have you tried any troubleshooting steps?"]) \
421
434
  .set_step_criteria("Issue type has been identified") \
422
435
  .set_valid_contexts(["technical_support", "billing_support", "general_inquiry"])
423
436
 
424
437
  # Technical support context
425
- tech = contexts.create_context("technical_support")
426
- tech.create_step("technical_diagnosis") \
427
- .add_section("Role", "You are a technical support specialist") \
428
- .add_section("Instructions", "Help diagnose and resolve technical issues") \
438
+ tech = contexts.add_context("technical_support")
439
+ tech.add_step("technical_diagnosis") \
440
+ .add_section("Current Task", "Help diagnose and resolve technical issues") \
441
+ .add_section("Available Tools", "Use web search to find solutions and datetime to check service windows") \
429
442
  .set_functions(["web_search", "datetime"]) # Can search for solutions and check times \
430
443
  .set_step_criteria("Technical issue is resolved or escalated") \
431
444
  .set_valid_contexts(["triage"]) # Can return to triage
432
445
 
433
446
  # Billing support context
434
- billing = contexts.create_context("billing_support")
435
- billing.create_step("billing_assistance") \
447
+ billing = contexts.add_context("billing_support")
448
+ billing.add_step("billing_assistance") \
436
449
  .set_text("I'll help you with your billing inquiry. Please provide your account details.") \
437
450
  .set_functions("none") # No external tools for sensitive billing info \
438
451
  .set_step_criteria("Billing issue is addressed") \
439
452
  .set_valid_contexts(["triage"])
440
453
 
441
454
  # General inquiry context
442
- general = contexts.create_context("general_inquiry")
443
- general.create_step("general_help") \
455
+ general = contexts.add_context("general_inquiry")
456
+ general.add_step("general_help") \
444
457
  .set_text("I'm here to help with general questions. What can I assist you with?") \
445
458
  .set_functions(["web_search", "datetime"]) # Full access to search and time \
446
459
  .set_step_criteria("Inquiry has been answered") \
@@ -1,12 +1,12 @@
1
- signalwire_agents/__init__.py,sha256=uXl3_WmqkymgMz9EDcAcLEXVXEbdL928PiK8--bJvYI,2707
1
+ signalwire_agents/__init__.py,sha256=3YN2p2ynw-LH_LXSsuM52E0U5fCBzjAoxlOaQ7H1bvA,2707
2
2
  signalwire_agents/agent_server.py,sha256=3Or8rIMAqW750V-XitBUMgOpW9BAIXmKXoGq7LkejAA,24988
3
3
  signalwire_agents/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
4
4
  signalwire_agents/cli/__init__.py,sha256=Iy2BfWDWBEZoA1cyHTDsooBSVMx4vH5Ddhr3sEuFe8c,197
5
- signalwire_agents/cli/build_search.py,sha256=PnGoIZVfIbSF21rb0m0_ceIL-8lJx59_qXrrl8l2yBE,22340
5
+ signalwire_agents/cli/build_search.py,sha256=KLQJBqVSADFyGcKAi0KLwU_UoUd5rtRoKAdfcH91u70,28652
6
6
  signalwire_agents/cli/test_swaig.py,sha256=CqfdBWE8_fiFnFWHbv9ouU1eUA_rGUUHwyOf_iUaZFU,101851
7
7
  signalwire_agents/core/__init__.py,sha256=mVDLbpq1pg_WwiqsQR28NNZwJ6-VUXFIfg-vN7pk0ew,806
8
- signalwire_agents/core/agent_base.py,sha256=0MA1i6honMKfyEIyCrVgS7WAR-iL3v3n8u0ZI8B-hHA,155944
9
- signalwire_agents/core/contexts.py,sha256=h7hra4xoiKAUdVyJhcKggl8X9EoqwTVWBmNMp-sEsuc,9598
8
+ signalwire_agents/core/agent_base.py,sha256=O-sQ9k6L8_qE94Xzpds3bSS52onxqVs8rYU9_1B0LV8,159349
9
+ signalwire_agents/core/contexts.py,sha256=RQGIZGR92AC90nv8_hGa8S4YGY-SmMa1giR4g7jLgQI,21283
10
10
  signalwire_agents/core/data_map.py,sha256=U-HLEZQomWf-UI0-nLAE8g1oyRdE5bU_WxQpboI2YI4,17695
11
11
  signalwire_agents/core/function_result.py,sha256=GGmd2gAB9uJFFyldrhC86LCmpCnccU1bb-3gZVW2rxE,45360
12
12
  signalwire_agents/core/logging_config.py,sha256=x4d_RAjBjVpJOFA2vXnPP2dNr13BZHz091J5rGpC77Y,13142
@@ -15,7 +15,7 @@ signalwire_agents/core/skill_base.py,sha256=lOpVTLhD9NjStF7Lxh6bAQUGa3DpNYV4agXJ
15
15
  signalwire_agents/core/skill_manager.py,sha256=XWq4MeDQ3kmM4Th8qI-Gx5klokDHNZ-K9cChJPW-zmQ,8113
16
16
  signalwire_agents/core/swaig_function.py,sha256=3xJrrQVxCZX-DssLkdjaui_psTUzahkzAsQ1EyRVMFk,6837
17
17
  signalwire_agents/core/swml_builder.py,sha256=Q1ikU9pedgjW888mjbqDFv-jMDvDZ-tZgfyMfu4qQN0,6719
18
- signalwire_agents/core/swml_handler.py,sha256=C8NvMpNdFg9UiEzPwMmMXMn8X6w10IShKjBJ8VSITBk,8189
18
+ signalwire_agents/core/swml_handler.py,sha256=hFDq41dQWL3EdFbq6h0hizE1dIqdVeiTeCrujbZsPzo,8397
19
19
  signalwire_agents/core/swml_renderer.py,sha256=u6HnbOC0NUnPpr6uz0FkXNEqcTF2OjWDui6gceD1Bhs,14773
20
20
  signalwire_agents/core/swml_service.py,sha256=oLdVYRYDyucgBVDAuhiOWVTN6p21SvPvddkPIMMd5kw,48346
21
21
  signalwire_agents/core/security/__init__.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
@@ -50,18 +50,18 @@ signalwire_agents/skills/math/skill.py,sha256=5sErd5x1rFHJg2GlmdJB3LvrmvTNOrZsA2
50
50
  signalwire_agents/skills/native_vector_search/__init__.py,sha256=buvncVoH5u8MJA0SLlz1JQgIuyBTQW5aql-ydnc7Wh8,29
51
51
  signalwire_agents/skills/native_vector_search/skill.py,sha256=da-3eM1NT7Tcju1GiQipxOrYhP7OIry68l2dLKpDV4I,15859
52
52
  signalwire_agents/skills/web_search/__init__.py,sha256=wJlptYDExYw-nxZJVzlTLOgkKkDOLUUt1ZdoLt44ixs,45
53
- signalwire_agents/skills/web_search/skill.py,sha256=GkfhG3Vz2HxOv91TvVuA4e_4b5cuswUpnJDLdaZW37k,10304
54
- signalwire_agents/skills/wikipedia/__init__.py,sha256=8Db_aE0ly7QoXg7n2RDvCqKupkyR-UYlK9uFUnGNCE8,184
55
- signalwire_agents/skills/wikipedia/skill.py,sha256=Q_HWJoG2RkQuZMgWxD9czuzEor79Gy1tjo6ywVzec84,6877
53
+ signalwire_agents/skills/web_search/skill.py,sha256=6EwoNABxEH5UkEdXsPT72PQzoVlFUbWsFJR6NuyhglI,10363
54
+ signalwire_agents/skills/wikipedia_search/__init__.py,sha256=8Db_aE0ly7QoXg7n2RDvCqKupkyR-UYlK9uFUnGNCE8,184
55
+ signalwire_agents/skills/wikipedia_search/skill.py,sha256=Q_HWJoG2RkQuZMgWxD9czuzEor79Gy1tjo6ywVzec84,6877
56
56
  signalwire_agents/utils/__init__.py,sha256=1KVsHzwgfktSXHe3vqSRGImjtIE58szwD2FHHoFBtvY,601
57
57
  signalwire_agents/utils/pom_utils.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
58
58
  signalwire_agents/utils/schema_utils.py,sha256=i4okv_O9bUApwT_jJf4Yoij3bLCrGrW3DC-vzSy2RuY,16392
59
59
  signalwire_agents/utils/token_generators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
60
60
  signalwire_agents/utils/validators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
61
- signalwire_agents-0.1.16.data/data/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
62
- signalwire_agents-0.1.16.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
63
- signalwire_agents-0.1.16.dist-info/METADATA,sha256=Wa9zmHC-iN-0ftjlPqeLdcjDsAhLpBT2b2WBCeajnkY,34572
64
- signalwire_agents-0.1.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
- signalwire_agents-0.1.16.dist-info/entry_points.txt,sha256=LRwltbVfaKUFMYmQoMxJGTT_-iQm0ftzXK0xPfD64Is,138
66
- signalwire_agents-0.1.16.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
67
- signalwire_agents-0.1.16.dist-info/RECORD,,
61
+ signalwire_agents-0.1.18.data/data/schema.json,sha256=M8Mn6pQda2P9jhbmkALrLr1wt-fRuhYRqdmEi9Rbhqk,178075
62
+ signalwire_agents-0.1.18.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
63
+ signalwire_agents-0.1.18.dist-info/METADATA,sha256=OdI3xK6tg17jbWrZkWhAN4hsK2G67oG4VYSQ5BVTIMA,35364
64
+ signalwire_agents-0.1.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
65
+ signalwire_agents-0.1.18.dist-info/entry_points.txt,sha256=LRwltbVfaKUFMYmQoMxJGTT_-iQm0ftzXK0xPfD64Is,138
66
+ signalwire_agents-0.1.18.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
67
+ signalwire_agents-0.1.18.dist-info/RECORD,,