signalwire-agents 1.0.14__py3-none-any.whl → 1.0.16__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__ = "1.0.14"
21
+ __version__ = "1.0.16"
22
22
 
23
23
  # Import core classes for easier access
24
24
  from .core.agent_base import AgentBase
@@ -76,6 +76,13 @@ class AgentServer:
76
76
  # Register health endpoints immediately so they're available
77
77
  # whether using server.run() or server.app with gunicorn
78
78
  self._register_health_endpoints()
79
+
80
+ # Register catch-all handler on startup (not in __init__) so it runs AFTER
81
+ # all other routes are registered. This ensures custom routes like /get_token
82
+ # don't get overshadowed by the catch-all /{full_path:path} route.
83
+ @self.app.on_event("startup")
84
+ async def _setup_catch_all():
85
+ self._register_catch_all_handler()
79
86
 
80
87
  def register(self, agent: AgentBase, route: Optional[str] = None) -> None:
81
88
  """
@@ -104,12 +111,12 @@ class AgentServer:
104
111
 
105
112
  # Store the agent
106
113
  self.agents[route] = agent
107
-
114
+
108
115
  # Get the router and register it using the standard approach
109
116
  # The agent's router already handles both trailing slash versions properly
110
117
  router = agent.as_router()
111
118
  self.app.include_router(router, prefix=route)
112
-
119
+
113
120
  self.logger.info(f"Registered agent '{agent.get_name()}' at route '{route}'")
114
121
 
115
122
  # If SIP routing is enabled and auto-mapping is on, register SIP usernames for this agent
@@ -550,9 +557,6 @@ class AgentServer:
550
557
  if not self.agents:
551
558
  self.logger.warning("Starting server with no registered agents")
552
559
 
553
- # Register catch-all route handler (handles agent routing and static files)
554
- self._register_catch_all_handler()
555
-
556
560
  # Set host and port
557
561
  host = host or self.host
558
562
  port = port or self.port
@@ -682,9 +686,6 @@ class AgentServer:
682
686
 
683
687
  self.logger.info(f"Serving static files from '{directory}' at route '{route or '/'}'")
684
688
 
685
- # Register catch-all handler immediately so it works with gunicorn
686
- # (when using server.app directly instead of server.run())
687
- self._register_catch_all_handler()
688
689
 
689
690
  def _serve_static_file(self, file_path: str, route: str = "/") -> Optional[Response]:
690
691
  """
@@ -742,14 +743,10 @@ class AgentServer:
742
743
  1. Routing requests without trailing slashes to agents (e.g., /santa instead of /santa/)
743
744
  2. Serving static files from directories registered with serve_static_files()
744
745
 
745
- The handler is registered once and works with both server.run() and
746
- direct use of server.app with gunicorn/uvicorn.
746
+ Called via startup event (not __init__) to ensure it runs AFTER all other routes
747
+ are registered. This prevents the catch-all from overshadowing custom routes
748
+ like /get_token that users may add to server.app.
747
749
  """
748
- # Only register once
749
- if getattr(self, '_catch_all_registered', False):
750
- return
751
- self._catch_all_registered = True
752
-
753
750
  @self.app.get("/{full_path:path}")
754
751
  @self.app.post("/{full_path:path}")
755
752
  async def handle_all_routes(request: Request, full_path: str):
@@ -92,7 +92,7 @@ PROCFILE_TEMPLATE = """web: gunicorn app:app --bind 0.0.0.0:$PORT --workers 2 --
92
92
  RUNTIME_TEMPLATE = """python-3.11
93
93
  """
94
94
 
95
- REQUIREMENTS_TEMPLATE = """signalwire-agents>=1.0.13
95
+ REQUIREMENTS_TEMPLATE = """signalwire-agents>=1.0.16
96
96
  gunicorn>=21.0.0
97
97
  uvicorn>=0.24.0
98
98
  python-dotenv>=1.0.0
@@ -150,8 +150,8 @@ SIGNALWIRE_TOKEN=your-api-token
150
150
  # This should be your publicly accessible URL (e.g., ngrok, dokku domain)
151
151
  SWML_PROXY_URL_BASE=https://your-app.example.com
152
152
 
153
- # Basic Auth for SWML endpoints (recommended)
154
- SWML_BASIC_AUTH_USER=admin
153
+ # Basic Auth for SWML endpoints (password required, user defaults to 'signalwire')
154
+ # SWML_BASIC_AUTH_USER=signalwire
155
155
  SWML_BASIC_AUTH_PASSWORD=your-secure-password
156
156
 
157
157
  # Agent Configuration
@@ -383,8 +383,8 @@ def setup_swml_handler():
383
383
  project = os.getenv("SIGNALWIRE_PROJECT_ID", "")
384
384
  token = os.getenv("SIGNALWIRE_TOKEN", "")
385
385
  agent_name = os.getenv("AGENT_NAME", "{agent_slug}")
386
- proxy_url = os.getenv("SWML_PROXY_URL_BASE", "")
387
- auth_user = os.getenv("SWML_BASIC_AUTH_USER", "")
386
+ proxy_url = os.getenv("SWML_PROXY_URL_BASE", os.getenv("APP_URL", ""))
387
+ auth_user = os.getenv("SWML_BASIC_AUTH_USER", "signalwire")
388
388
  auth_pass = os.getenv("SWML_BASIC_AUTH_PASSWORD", "")
389
389
 
390
390
  if not all([sw_host, project, token]):
@@ -395,8 +395,8 @@ def setup_swml_handler():
395
395
  print("SWML_PROXY_URL_BASE not set - skipping SWML handler setup")
396
396
  return
397
397
 
398
- # Build SWML URL with basic auth
399
- if auth_user and auth_pass and "://" in proxy_url:
398
+ # Build SWML URL with basic auth (user defaults to 'signalwire' if not set)
399
+ if auth_pass and "://" in proxy_url:
400
400
  scheme, rest = proxy_url.split("://", 1)
401
401
  swml_url = f"{{scheme}}://{{auth_user}}:{{auth_pass}}@{{rest}}/swml"
402
402
  else:
@@ -411,19 +411,17 @@ def setup_swml_handler():
411
411
  swml_handler_info["address_id"] = existing["address_id"]
412
412
  swml_handler_info["address"] = existing["address"]
413
413
 
414
- if existing.get("url") != swml_url:
415
- try:
416
- requests.put(
417
- f"https://{{sw_host}}/api/fabric/resources/external_swml_handlers/{{existing['id']}}",
418
- json={{"primary_request_url": swml_url, "primary_request_method": "POST"}},
419
- auth=auth,
420
- headers=headers
421
- )
422
- print(f"Updated SWML handler: {{existing['name']}}")
423
- except Exception as e:
424
- print(f"Failed to update handler URL: {{e}}")
425
- else:
426
- print(f"Using existing SWML handler: {{existing['name']}}")
414
+ # Always update the URL on startup to ensure credentials are current
415
+ try:
416
+ requests.put(
417
+ f"https://{{sw_host}}/api/fabric/resources/external_swml_handlers/{{existing['id']}}",
418
+ json={{"primary_request_url": swml_url, "primary_request_method": "POST"}},
419
+ auth=auth,
420
+ headers=headers
421
+ )
422
+ print(f"Updated SWML handler: {{existing['name']}}")
423
+ except Exception as e:
424
+ print(f"Failed to update handler URL: {{e}}")
427
425
  print(f"Call address: {{existing['address']}}")
428
426
  else:
429
427
  try:
@@ -1111,9 +1109,11 @@ curl -X POST <span class="base-url"></span>/swml/swaig/ \\
1111
1109
  return div.innerHTML;
1112
1110
  }}
1113
1111
 
1114
- // WebRTC calling
1112
+ // WebRTC calling - robust pattern from santa app.js
1115
1113
  let client = null;
1116
1114
  let roomSession = null;
1115
+ let currentToken = null;
1116
+ let currentDestination = null;
1117
1117
 
1118
1118
  const connectBtn = document.getElementById('connectBtn');
1119
1119
  const disconnectBtn = document.getElementById('disconnectBtn');
@@ -1125,8 +1125,15 @@ curl -X POST <span class="base-url"></span>/swml/swaig/ \\
1125
1125
  }}
1126
1126
 
1127
1127
  async function connect() {{
1128
+ // Debounce - prevent double-clicks
1129
+ if (connectBtn.disabled) {{
1130
+ console.log('Call already in progress');
1131
+ return;
1132
+ }}
1133
+ connectBtn.disabled = true;
1134
+ connectBtn.textContent = 'Connecting...';
1135
+
1128
1136
  try {{
1129
- connectBtn.disabled = true;
1130
1137
  updateCallStatus('Getting token...');
1131
1138
 
1132
1139
  const tokenResp = await fetch('/get_token');
@@ -1136,17 +1143,28 @@ curl -X POST <span class="base-url"></span>/swml/swaig/ \\
1136
1143
  throw new Error(tokenData.error);
1137
1144
  }}
1138
1145
 
1146
+ currentToken = tokenData.token;
1147
+ currentDestination = tokenData.address;
1148
+
1139
1149
  if (tokenData.address) {{
1140
1150
  destinationInput.value = tokenData.address;
1141
1151
  }}
1142
1152
 
1153
+ console.log('Got token, destination:', currentDestination);
1143
1154
  updateCallStatus('Connecting...');
1144
1155
 
1145
- client = await window.SignalWire.SignalWire({{
1146
- token: tokenData.token
1147
- }});
1156
+ // Initialize SignalWire client
1157
+ if (window.SignalWire && typeof window.SignalWire.SignalWire === 'function') {{
1158
+ console.log('Initializing SignalWire client...');
1159
+ client = await window.SignalWire.SignalWire({{
1160
+ token: currentToken
1161
+ }});
1162
+ }} else {{
1163
+ console.error('SignalWire SDK structure:', window.SignalWire);
1164
+ throw new Error('SignalWire.SignalWire function not found');
1165
+ }}
1148
1166
 
1149
- const destination = tokenData.address || destinationInput.value;
1167
+ const destination = currentDestination || destinationInput.value;
1150
1168
  roomSession = await client.dial({{
1151
1169
  to: destination,
1152
1170
  audio: {{
@@ -1157,22 +1175,37 @@ curl -X POST <span class="base-url"></span>/swml/swaig/ \\
1157
1175
  video: false
1158
1176
  }});
1159
1177
 
1178
+ console.log('Room session created:', roomSession);
1179
+
1160
1180
  roomSession.on('call.joined', () => {{
1181
+ console.log('Call joined');
1161
1182
  updateCallStatus('Connected');
1162
1183
  disconnectBtn.disabled = false;
1163
1184
  }});
1164
1185
 
1165
1186
  roomSession.on('call.left', () => {{
1187
+ console.log('Call left');
1166
1188
  updateCallStatus('Call ended');
1167
1189
  cleanup();
1168
1190
  }});
1169
1191
 
1170
1192
  roomSession.on('destroy', () => {{
1193
+ console.log('Session destroyed');
1171
1194
  updateCallStatus('Call ended');
1172
1195
  cleanup();
1173
1196
  }});
1174
1197
 
1198
+ roomSession.on('room.left', () => {{
1199
+ console.log('Room left');
1200
+ cleanup();
1201
+ }});
1202
+
1175
1203
  await roomSession.start();
1204
+ console.log('Call started successfully');
1205
+
1206
+ // Update UI
1207
+ connectBtn.style.display = 'none';
1208
+ disconnectBtn.style.display = 'inline-block';
1176
1209
 
1177
1210
  }} catch (err) {{
1178
1211
  console.error('Connection error:', err);
@@ -1182,26 +1215,65 @@ curl -X POST <span class="base-url"></span>/swml/swaig/ \\
1182
1215
  }}
1183
1216
 
1184
1217
  async function disconnect() {{
1218
+ console.log('Disconnect called');
1219
+ await hangup();
1220
+ }}
1221
+
1222
+ async function hangup() {{
1185
1223
  try {{
1186
1224
  if (roomSession) {{
1225
+ console.log('Hanging up call...');
1187
1226
  await roomSession.hangup();
1227
+ console.log('Call hung up successfully');
1188
1228
  }}
1189
1229
  }} catch (err) {{
1190
- console.error('Disconnect error:', err);
1230
+ console.error('Hangup error:', err);
1191
1231
  }}
1192
1232
  cleanup();
1193
1233
  }}
1194
1234
 
1195
1235
  function cleanup() {{
1236
+ console.log('Cleanup called');
1237
+
1238
+ // Clean up local stream if it exists
1239
+ if (roomSession && roomSession.localStream) {{
1240
+ console.log('Stopping local stream tracks');
1241
+ roomSession.localStream.getTracks().forEach(track => {{
1242
+ track.stop();
1243
+ }});
1244
+ }}
1245
+
1246
+ roomSession = null;
1247
+
1248
+ // Disconnect the client properly
1249
+ if (client) {{
1250
+ try {{
1251
+ console.log('Disconnecting client');
1252
+ client.disconnect();
1253
+ }} catch (e) {{
1254
+ console.log('Client disconnect error:', e);
1255
+ }}
1256
+ client = null;
1257
+ }}
1258
+
1259
+ // Reset UI
1196
1260
  connectBtn.disabled = false;
1261
+ connectBtn.textContent = 'Call Agent';
1262
+ connectBtn.style.display = 'inline-block';
1197
1263
  disconnectBtn.disabled = true;
1198
- roomSession = null;
1199
- client = null;
1264
+ disconnectBtn.style.display = 'none';
1200
1265
  }}
1201
1266
 
1202
1267
  connectBtn.addEventListener('click', connect);
1203
1268
  disconnectBtn.addEventListener('click', disconnect);
1204
1269
 
1270
+ // Clean up on page unload
1271
+ window.addEventListener('beforeunload', () => {{
1272
+ if (roomSession) {{
1273
+ hangup();
1274
+ }}
1275
+ }});
1276
+
1205
1277
  // Initialize on load
1206
1278
  document.addEventListener('DOMContentLoaded', async function() {{
1207
1279
  const baseUrl = window.location.origin;
@@ -1448,7 +1520,7 @@ swaig-test app.py --list-tools
1448
1520
  # Templates - CI/CD Mode
1449
1521
  # =============================================================================
1450
1522
 
1451
- DEPLOY_WORKFLOW_TEMPLATE = '''# Auto-deploy to Dokku on push
1523
+ DEPLOY_WORKFLOW_TEMPLATE = '''# Deploy to Dokku - calls reusable workflow from dokku-deploy-system
1452
1524
  name: Deploy
1453
1525
 
1454
1526
  on:
@@ -1456,138 +1528,24 @@ on:
1456
1528
  push:
1457
1529
  branches: [main, staging, develop]
1458
1530
 
1531
+ permissions:
1532
+ contents: read
1533
+ deployments: write
1534
+
1459
1535
  concurrency:
1460
1536
  group: deploy-${{{{ github.ref }}}}
1461
1537
  cancel-in-progress: true
1462
1538
 
1463
1539
  jobs:
1464
1540
  deploy:
1465
- runs-on: ubuntu-latest
1466
- environment: ${{{{ github.ref_name == 'main' && 'production' || github.ref_name == 'staging' && 'staging' || 'development' }}}}
1467
- env:
1468
- BASE_APP_NAME: ${{{{ github.event.repository.name }}}}
1469
- steps:
1470
- - uses: actions/checkout@v4
1471
- with:
1472
- fetch-depth: 0
1473
-
1474
- - name: Set variables
1475
- id: vars
1476
- run: |
1477
- BRANCH="${{GITHUB_REF#refs/heads/}}"
1478
- case "$BRANCH" in
1479
- main) APP="${{BASE_APP_NAME}}"; ENV="production" ;;
1480
- staging) APP="${{BASE_APP_NAME}}-staging"; ENV="staging" ;;
1481
- develop) APP="${{BASE_APP_NAME}}-dev"; ENV="development" ;;
1482
- *) APP="${{BASE_APP_NAME}}"; ENV="production" ;;
1483
- esac
1484
- echo "app_name=$APP" >> $GITHUB_OUTPUT
1485
- echo "environment=$ENV" >> $GITHUB_OUTPUT
1486
-
1487
- - name: Setup SSH
1488
- run: |
1489
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
1490
- echo "${{{{ secrets.DOKKU_SSH_PRIVATE_KEY }}}}" > ~/.ssh/key && chmod 600 ~/.ssh/key
1491
- ssh-keyscan -H ${{{{ secrets.DOKKU_HOST }}}} >> ~/.ssh/known_hosts
1492
- echo -e "Host dokku\\n HostName ${{{{ secrets.DOKKU_HOST }}}}\\n User dokku\\n IdentityFile ~/.ssh/key" > ~/.ssh/config
1493
-
1494
- - name: Create app
1495
- run: |
1496
- APP_NAME="${{{{ steps.vars.outputs.app_name }}}}"
1497
- ssh dokku apps:exists $APP_NAME 2>/dev/null || ssh dokku apps:create $APP_NAME
1498
-
1499
- - name: Unlock app
1500
- run: |
1501
- APP_NAME="${{{{ steps.vars.outputs.app_name }}}}"
1502
- ssh dokku apps:unlock $APP_NAME 2>/dev/null || true
1503
-
1504
- - name: Configure env
1505
- run: |
1506
- APP_NAME="${{{{ steps.vars.outputs.app_name }}}}"
1507
- DOMAIN="${{APP_NAME}}.${{{{ secrets.BASE_DOMAIN }}}}"
1508
- ssh dokku config:set --no-restart $APP_NAME \\
1509
- APP_ENV="${{{{ steps.vars.outputs.environment }}}}" \\
1510
- APP_URL="https://${{DOMAIN}}" \\
1511
- SWML_BASIC_AUTH_USER="${{{{ secrets.SWML_BASIC_AUTH_USER }}}}" \\
1512
- SWML_BASIC_AUTH_PASSWORD="${{{{ secrets.SWML_BASIC_AUTH_PASSWORD }}}}"
1513
-
1514
- - name: Deploy
1515
- run: |
1516
- APP_NAME="${{{{ steps.vars.outputs.app_name }}}}"
1517
- git remote add dokku dokku@${{{{ secrets.DOKKU_HOST }}}}:$APP_NAME 2>/dev/null || true
1518
- GIT_SSH_COMMAND="ssh -i ~/.ssh/key" git push dokku HEAD:main -f
1519
-
1520
- - name: Configure domain
1521
- run: |
1522
- APP_NAME="${{{{ steps.vars.outputs.app_name }}}}"
1523
- DOMAIN="${{APP_NAME}}.${{{{ secrets.BASE_DOMAIN }}}}"
1524
- ssh dokku domains:clear $APP_NAME 2>/dev/null || true
1525
- ssh dokku domains:add $APP_NAME $DOMAIN
1526
-
1527
- - name: SSL
1528
- run: |
1529
- APP_NAME="${{{{ steps.vars.outputs.app_name }}}}"
1530
- echo "Checking SSL status..."
1531
- SSL_STATUS=$(ssh dokku letsencrypt:active $APP_NAME 2>/dev/null || echo "error")
1532
- echo "SSL status: $SSL_STATUS"
1533
- if [ "$SSL_STATUS" = "true" ]; then
1534
- echo "SSL already active"
1535
- else
1536
- echo "Enabling SSL..."
1537
- ssh dokku letsencrypt:enable $APP_NAME 2>&1 || echo "SSL enable failed"
1538
- fi
1539
-
1540
- - name: Verify
1541
- id: verify
1542
- run: |
1543
- APP_NAME="${{{{ steps.vars.outputs.app_name }}}}"
1544
- DOMAIN="${{APP_NAME}}.${{{{ secrets.BASE_DOMAIN }}}}"
1545
- sleep 10
1546
- if curl -sf "https://${{DOMAIN}}/health"; then
1547
- echo "HTTPS OK: https://${{DOMAIN}}"
1548
- echo "status=success" >> $GITHUB_OUTPUT
1549
- echo "url=https://${{DOMAIN}}" >> $GITHUB_OUTPUT
1550
- elif curl -sf "http://${{DOMAIN}}/health"; then
1551
- echo "HTTP only: http://${{DOMAIN}}"
1552
- echo "status=success" >> $GITHUB_OUTPUT
1553
- echo "url=http://${{DOMAIN}}" >> $GITHUB_OUTPUT
1554
- else
1555
- echo "Check logs"
1556
- echo "status=failed" >> $GITHUB_OUTPUT
1557
- fi
1558
-
1559
- - name: Notify Slack
1560
- if: always()
1561
- env:
1562
- SLACK_WEBHOOK_URL: ${{{{ secrets.SLACK_WEBHOOK_URL }}}}
1563
- run: |
1564
- [ -z "$SLACK_WEBHOOK_URL" ] && exit 0
1565
- APP_NAME="${{{{ steps.vars.outputs.app_name }}}}"
1566
- DOMAIN="${{APP_NAME}}.${{{{ secrets.BASE_DOMAIN }}}}"
1567
- if [ "${{{{ steps.verify.outputs.status }}}}" == "success" ]; then
1568
- COLOR="good"
1569
- STATUS="✅ Deployed"
1570
- else
1571
- COLOR="danger"
1572
- STATUS="❌ Deploy failed"
1573
- fi
1574
- curl -X POST -H 'Content-type: application/json' \\
1575
- --data "{{
1576
- \\"attachments\\": [{{
1577
- \\"color\\": \\"$COLOR\\",
1578
- \\"title\\": \\"$STATUS: $APP_NAME\\",
1579
- \\"fields\\": [
1580
- {{\\"title\\": \\"Environment\\", \\"value\\": \\"${{{{ steps.vars.outputs.environment }}}}\\", \\"short\\": true}},
1581
- {{\\"title\\": \\"Branch\\", \\"value\\": \\"${{{{ github.ref_name }}}}\\", \\"short\\": true}},
1582
- {{\\"title\\": \\"URL\\", \\"value\\": \\"https://$DOMAIN\\", \\"short\\": false}}
1583
- ],
1584
- \\"footer\\": \\"<${{{{ github.server_url }}}}/${{{{ github.repository }}}}/actions/runs/${{{{ github.run_id }}}}|View Workflow>\\"
1585
- }}]
1586
- }}" \\
1587
- "$SLACK_WEBHOOK_URL" || true
1541
+ uses: signalwire-demos/dokku-deploy-system/.github/workflows/deploy.yml@main
1542
+ secrets: inherit
1543
+ # Optional: customize health check path
1544
+ # with:
1545
+ # health_check_path: '/health'
1588
1546
  '''
1589
1547
 
1590
- PREVIEW_WORKFLOW_TEMPLATE = '''# Preview environments for pull requests
1548
+ PREVIEW_WORKFLOW_TEMPLATE = '''# Preview environments for pull requests - calls reusable workflow from dokku-deploy-system
1591
1549
  name: Preview
1592
1550
 
1593
1551
  on:
@@ -1597,74 +1555,13 @@ on:
1597
1555
  concurrency:
1598
1556
  group: preview-${{{{ github.event.pull_request.number }}}}
1599
1557
 
1600
- env:
1601
- APP_NAME: ${{{{ github.event.repository.name }}}}-pr-${{{{ github.event.pull_request.number }}}}
1602
-
1603
1558
  jobs:
1604
- deploy:
1605
- if: github.event.action != 'closed'
1606
- runs-on: ubuntu-latest
1607
- environment: preview
1608
- steps:
1609
- - uses: actions/checkout@v4
1610
- with:
1611
- fetch-depth: 0
1612
-
1613
- - name: Setup SSH
1614
- run: |
1615
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
1616
- echo "${{{{ secrets.DOKKU_SSH_PRIVATE_KEY }}}}" > ~/.ssh/key && chmod 600 ~/.ssh/key
1617
- ssh-keyscan -H ${{{{ secrets.DOKKU_HOST }}}} >> ~/.ssh/known_hosts
1618
-
1619
- - name: Create app
1620
- run: |
1621
- ssh -i ~/.ssh/key dokku@${{{{ secrets.DOKKU_HOST }}}} apps:exists $APP_NAME 2>/dev/null || \\
1622
- ssh -i ~/.ssh/key dokku@${{{{ secrets.DOKKU_HOST }}}} apps:create $APP_NAME
1623
-
1624
- - name: Configure env
1625
- run: |
1626
- DOMAIN="${{{{ env.APP_NAME }}}}.${{{{ secrets.BASE_DOMAIN }}}}"
1627
- ssh -i ~/.ssh/key dokku@${{{{ secrets.DOKKU_HOST }}}} config:set --no-restart $APP_NAME \\
1628
- APP_ENV=preview \\
1629
- APP_URL="https://$DOMAIN"
1630
- ssh -i ~/.ssh/key dokku@${{{{ secrets.DOKKU_HOST }}}} resource:limit $APP_NAME --memory 256m || true
1631
-
1632
- - name: Deploy
1633
- run: |
1634
- git remote add dokku dokku@${{{{ secrets.DOKKU_HOST }}}}:$APP_NAME 2>/dev/null || true
1635
- GIT_SSH_COMMAND="ssh -i ~/.ssh/key" git push dokku HEAD:main -f
1636
-
1637
- - name: Configure domain
1638
- run: |
1639
- DOMAIN="${{{{ env.APP_NAME }}}}.${{{{ secrets.BASE_DOMAIN }}}}"
1640
- ssh -i ~/.ssh/key dokku@${{{{ secrets.DOKKU_HOST }}}} domains:clear $APP_NAME 2>/dev/null || true
1641
- ssh -i ~/.ssh/key dokku@${{{{ secrets.DOKKU_HOST }}}} domains:add $APP_NAME $DOMAIN
1642
-
1643
- - name: SSL
1644
- run: |
1645
- ssh -i ~/.ssh/key dokku@${{{{ secrets.DOKKU_HOST }}}} letsencrypt:enable $APP_NAME || true
1646
-
1647
- - name: Comment URL
1648
- uses: actions/github-script@v7
1649
- with:
1650
- script: |
1651
- const url = `https://${{{{ env.APP_NAME }}}}.${{{{ secrets.BASE_DOMAIN }}}}`;
1652
- const body = `## 🚀 Preview\\n\\n✅ Deployed: [${{url}}](${{url}})\\n\\n<sub>Auto-destroyed on PR close</sub>`;
1653
- const comments = await github.rest.issues.listComments({{owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number}});
1654
- const bot = comments.data.find(c => c.user.type === 'Bot' && c.body.includes('Preview'));
1655
- if (bot) await github.rest.issues.updateComment({{owner: context.repo.owner, repo: context.repo.repo, comment_id: bot.id, body}});
1656
- else await github.rest.issues.createComment({{owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body}});
1657
-
1658
- cleanup:
1659
- if: github.event.action == 'closed'
1660
- runs-on: ubuntu-latest
1661
- steps:
1662
- - name: Destroy preview
1663
- run: |
1664
- mkdir -p ~/.ssh
1665
- echo "${{{{ secrets.DOKKU_SSH_PRIVATE_KEY }}}}" > ~/.ssh/key && chmod 600 ~/.ssh/key
1666
- ssh-keyscan -H ${{{{ secrets.DOKKU_HOST }}}} >> ~/.ssh/known_hosts
1667
- ssh -i ~/.ssh/key dokku@${{{{ secrets.DOKKU_HOST }}}} apps:destroy ${{{{ env.APP_NAME }}}} --force || true
1559
+ preview:
1560
+ uses: signalwire-demos/dokku-deploy-system/.github/workflows/preview.yml@main
1561
+ secrets: inherit
1562
+ # Optional: customize memory limit for previews
1563
+ # with:
1564
+ # memory_limit: '256m'
1668
1565
  '''
1669
1566
 
1670
1567
  DOKKU_CONFIG_TEMPLATE = '''# ═══════════════════════════════════════════════════════════════════════════════
@@ -109,7 +109,7 @@ class AgentBase(
109
109
  name: str,
110
110
  route: str = "/",
111
111
  host: str = "0.0.0.0",
112
- port: int = 3000,
112
+ port: Optional[int] = None,
113
113
  basic_auth: Optional[Tuple[str, str]] = None,
114
114
  use_pom: bool = True,
115
115
  token_expiry_secs: int = 3600,
@@ -162,7 +162,8 @@ class AgentBase(
162
162
  # Apply service config values, with constructor parameters taking precedence
163
163
  final_route = route if route != "/" else service_config.get('route', route)
164
164
  final_host = host if host != "0.0.0.0" else service_config.get('host', host)
165
- final_port = port if port != 3000 else service_config.get('port', port)
165
+ # For port: use explicit param if provided, else config file, else let SWMLService use PORT env var
166
+ final_port = port if port is not None else service_config.get('port', None)
166
167
  final_name = service_config.get('name', name)
167
168
 
168
169
  # Initialize the SWMLService base class
@@ -182,7 +183,7 @@ class AgentBase(
182
183
 
183
184
  # Setup logger for this instance
184
185
  self.log = logger.bind(agent=name)
185
- self.log.info("agent_initializing", route=route, host=host, port=port)
186
+ self.log.info("agent_initializing", agent=name, route=route, host=self.host, port=self.port)
186
187
 
187
188
  # Store agent-specific parameters
188
189
  self._default_webhook_url = default_webhook_url
@@ -64,7 +64,7 @@ class SWMLService:
64
64
  name: str,
65
65
  route: str = "/",
66
66
  host: str = "0.0.0.0",
67
- port: int = 3000,
67
+ port: Optional[int] = None,
68
68
  basic_auth: Optional[Tuple[str, str]] = None,
69
69
  schema_path: Optional[str] = None,
70
70
  config_file: Optional[str] = None
@@ -84,7 +84,8 @@ class SWMLService:
84
84
  self.name = name
85
85
  self.route = route.rstrip("/") # Ensure no trailing slash
86
86
  self.host = host
87
- self.port = port
87
+ # Use provided port, or PORT env var, or default to 3000
88
+ self.port = port if port is not None else int(os.environ.get("PORT", 3000))
88
89
 
89
90
  # Initialize logger for this instance FIRST before using it
90
91
  self.log = logger.bind(service=name)
@@ -107,7 +108,7 @@ class SWMLService:
107
108
  proxy_url_base=self._proxy_url_base)
108
109
  self._proxy_detection_done = False
109
110
  self._proxy_debug = os.environ.get('SWML_PROXY_DEBUG', '').lower() in ('true', '1', 'yes')
110
- self.log.info("service_initializing", route=self.route, host=host, port=port)
111
+ self.log.info("service_initializing", route=self.route, host=self.host, port=self.port)
111
112
 
112
113
  # Set basic auth credentials
113
114
  if basic_auth is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalwire_agents
3
- Version: 1.0.14
3
+ Version: 1.0.16
4
4
  Summary: SignalWire AI Agents SDK
5
5
  Author-email: SignalWire Team <info@signalwire.com>
6
6
  License: MIT
@@ -1,11 +1,11 @@
1
- signalwire_agents/__init__.py,sha256=LLus0vh58wwELjWZMPFsFhOM3CvoCo4LI866TXdJXn4,5031
2
- signalwire_agents/agent_server.py,sha256=dOPaATEnw03rHsfH5sqmmGPF7uzFffcUJYpTksx7CBg,32356
1
+ signalwire_agents/__init__.py,sha256=V45feASkAUfqV6O_9oDDgh-uCsoFoR9nYuN0J9-8jEk,5031
2
+ signalwire_agents/agent_server.py,sha256=seNZqNy2XFx6lIaRfMhFz3xC-z8Y9OIRmc0H_BhrVHk,32346
3
3
  signalwire_agents/schema.json,sha256=YQv4-KiegE00XvxoLMKAml6aCGitnt3kBq31ECxTHK8,385886
4
4
  signalwire_agents/agents/bedrock.py,sha256=J582gooNtxtep4xdVOfyDzRtHp_XrurPMS93xf2Xod0,10836
5
5
  signalwire_agents/cli/__init__.py,sha256=XbxAQFaCIdGXIXJiriVBWoFPOJsC401u21588nO4TG8,388
6
6
  signalwire_agents/cli/build_search.py,sha256=wDuonVY9fcqspThRHmbArTuZLKsiStu-ozVExtqawA8,54937
7
7
  signalwire_agents/cli/config.py,sha256=2i4e0BArdKsaXxjeueYYRNke7GWicHPYC2wuitVrP7A,2541
8
- signalwire_agents/cli/dokku.py,sha256=BiTCrF7m-LpxN-95xVt-M6zzFjBedpJI2nbK2Vpxt6g,85342
8
+ signalwire_agents/cli/dokku.py,sha256=WuyqErWPZ-Wyqfxlzb3DyPhkyrSropUYzHtTuqzjGiM,80255
9
9
  signalwire_agents/cli/init_project.py,sha256=nNBPni3x1xzdJ5pAZsQRUYK1t1e64NzwUUkq-Uc3mzo,78835
10
10
  signalwire_agents/cli/swaig_test_wrapper.py,sha256=t63HQpEc1Up5AcysEHP1OsEQcgSMKH-9H1L2IhFso18,1533
11
11
  signalwire_agents/cli/test_swaig.py,sha256=-v-XjTUWZNxmMJuOF5_cB1Jz8x8emJoqgqS_8jLeT4Y,31487
@@ -26,7 +26,7 @@ signalwire_agents/cli/simulation/data_generation.py,sha256=pxa9aJ6XkI0O8yAIGvBTU
26
26
  signalwire_agents/cli/simulation/data_overrides.py,sha256=3_3pT6j-q2gRufPX2bZ1BrmY7u1IdloLooKAJil33vI,6319
27
27
  signalwire_agents/cli/simulation/mock_env.py,sha256=fvaR_xdLMm8AbpNUbTJOFG9THcti3Zds-0QNDbKMaYk,10249
28
28
  signalwire_agents/core/__init__.py,sha256=xjPq8DmUnWYUG28sd17n430VWPmMH9oZ9W14gYwG96g,806
29
- signalwire_agents/core/agent_base.py,sha256=PYlLu312TRnZVTePUSZTrd6Ed6uvy7K6BIxT3Sd6uNM,58739
29
+ signalwire_agents/core/agent_base.py,sha256=zZRykB8VD277_5_NZbYP7ZcpeqTdQ4yWlpNnLXOljEY,58883
30
30
  signalwire_agents/core/auth_handler.py,sha256=jXrof9WZ1W9qqlQT9WElcmSRafL2kG7207x5SqWN9MU,8481
31
31
  signalwire_agents/core/config_loader.py,sha256=rStVRRUaeMGrMc44ocr0diMQQARZhbKqwMqQ6kqUNos,8722
32
32
  signalwire_agents/core/contexts.py,sha256=g9FgOGMfGCUWlm57YZcv7CvOf-Ub9FdKZIOMu14ADfE,24428
@@ -41,7 +41,7 @@ signalwire_agents/core/swaig_function.py,sha256=KnUQ2g99kDSzOzD1PJ0Iqs8DeeZ6jDII
41
41
  signalwire_agents/core/swml_builder.py,sha256=tJBFDAVTENEfjGLp2h9_AKOYt5O9FrSYLI-nZZVwM1E,15604
42
42
  signalwire_agents/core/swml_handler.py,sha256=hFDq41dQWL3EdFbq6h0hizE1dIqdVeiTeCrujbZsPzo,8397
43
43
  signalwire_agents/core/swml_renderer.py,sha256=-WAB_5ss836a8nBo5zlb6SaQKFNF4XIo1odWIXM4eE8,6860
44
- signalwire_agents/core/swml_service.py,sha256=vgK_OScBlXdsr3dMe7pLUnm_cQVWtmNl_lu5guAKDlg,49851
44
+ signalwire_agents/core/swml_service.py,sha256=TCsn3g_kAxz1DfSiPygpBv6KTd6EEDaWplFVqZZnZw8,49995
45
45
  signalwire_agents/core/agent/__init__.py,sha256=qccTmLD9b24tZDAoIPEY6vJ2p1R_ArZ_ZCKbBlKvPQ8,239
46
46
  signalwire_agents/core/agent/config/__init__.py,sha256=5XvTfnYeeGdoLr4tJjbe1OhF26nOcR5VTDIhtMGCu3I,244
47
47
  signalwire_agents/core/agent/deployment/__init__.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
@@ -136,12 +136,12 @@ signalwire_agents/utils/token_generators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663
136
136
  signalwire_agents/utils/validators.py,sha256=4Mr7baQ_xR_hfJ72YxQRAT_GFa663YjFX_PumJ35Xds,191
137
137
  signalwire_agents/web/__init__.py,sha256=XE_pSTY9Aalzr7J7wqFth1Zr3cccQHPPcF5HWNrOpz8,383
138
138
  signalwire_agents/web/web_service.py,sha256=a2PSHJgX1tlZr0Iz1A1UouZjXEePJAZL632evvLVM38,21071
139
- signalwire_agents-1.0.14.data/data/share/man/man1/sw-agent-init.1,sha256=J4k5Oi74BnWCPCvsaw00vuyyqDPuIECjJIPu5OynvJc,8381
140
- signalwire_agents-1.0.14.data/data/share/man/man1/sw-search.1,sha256=9jJ6V6t6DgmXByz8Lw9exjf683Cw3sJGro8-eB0M9EY,10413
141
- signalwire_agents-1.0.14.data/data/share/man/man1/swaig-test.1,sha256=Ri0EITo8YMFowkcYltwPSwU4VJdRzo7XTWloi5WddCg,7815
142
- signalwire_agents-1.0.14.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
143
- signalwire_agents-1.0.14.dist-info/METADATA,sha256=eqJAF8GpapKMkB059C3t7emIKbHJg-C5q6Mn7yjtqdQ,41740
144
- signalwire_agents-1.0.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
145
- signalwire_agents-1.0.14.dist-info/entry_points.txt,sha256=fMiBH-GLeXGaWWn58Mcj7KM_m3SdomQMUQu-1LTqscw,315
146
- signalwire_agents-1.0.14.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
147
- signalwire_agents-1.0.14.dist-info/RECORD,,
139
+ signalwire_agents-1.0.16.data/data/share/man/man1/sw-agent-init.1,sha256=J4k5Oi74BnWCPCvsaw00vuyyqDPuIECjJIPu5OynvJc,8381
140
+ signalwire_agents-1.0.16.data/data/share/man/man1/sw-search.1,sha256=9jJ6V6t6DgmXByz8Lw9exjf683Cw3sJGro8-eB0M9EY,10413
141
+ signalwire_agents-1.0.16.data/data/share/man/man1/swaig-test.1,sha256=Ri0EITo8YMFowkcYltwPSwU4VJdRzo7XTWloi5WddCg,7815
142
+ signalwire_agents-1.0.16.dist-info/licenses/LICENSE,sha256=NYvAsB-rTcSvG9cqHt9EUHAWLiA9YzM4Qfz-mPdvDR0,1067
143
+ signalwire_agents-1.0.16.dist-info/METADATA,sha256=k6ccgQKIyiYHtoXP3iqu436PQKrSLvB5rGlUliLJZ24,41740
144
+ signalwire_agents-1.0.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
145
+ signalwire_agents-1.0.16.dist-info/entry_points.txt,sha256=fMiBH-GLeXGaWWn58Mcj7KM_m3SdomQMUQu-1LTqscw,315
146
+ signalwire_agents-1.0.16.dist-info/top_level.txt,sha256=kDGS6ZYv84K9P5Kyg9_S8P_pbUXoHkso0On_DB5bbWc,18
147
+ signalwire_agents-1.0.16.dist-info/RECORD,,