aethexDocs

Démarrage rapide

Parlez à votre premier agent vocal en moins de 2 minutes.

Avant de commencer

Vous aurez besoin de Python 3.10+ et d'un navigateur moderne avec un microphone (pour l'étape 3). Les scripts du démarrage rapide importent requests et flask ; installez donc les deux dans un environnement virtuel au préalable — choisissez votre système ci-dessous. Lorsque vous exécutez les scripts dans les étapes suivantes, définissez les variables d'environnement en ligne sur macOS et Linux (AETHEX_API_KEY=ae_live_... python3 create_agent.py) ; sous Windows, définissez-les d'abord sur une ligne distincte avec $env:AETHEX_API_KEY="ae_live_..." (PowerShell) ou set AETHEX_API_KEY=ae_live_... (cmd), puis exécutez py create_agent.py. Gardez ce terminal ouvert pour les étapes 2 et 3 ; si vous en ouvrez un nouveau, réactivez l'environnement avec source .venv/bin/activate (macOS/Linux) ou .venv\Scripts\Activate.ps1 (Windows) avant d'exécuter un script.

bash
# Check you have Python 3.10+
python3 --version

# Create an isolated environment, then install the two
# libraries the quickstart scripts import.
python3 -m venv .venv
source .venv/bin/activate
pip install requests flask

1. Obtenez votre clé API

Inscrivez-vous sur developers.aethexai.com, créez une clé et copiez-la. Les clés ressemblent à ae_live_....

2. Créez votre agent

Le script ci-dessous choisit une voix anglophone dans votre catalogue, crée un agent qui l'utilise et affiche le nouvel identifiant d'agent. Sauvegardez-le, exécutez-le, puis récupérez l'identifiant dans la sortie pour l'étape 3.

create_agent.py
python
# Run with:
#   AETHEX_API_KEY=ae_live_... python3 create_agent.py

import os
import requests

API_KEY = os.environ["AETHEX_API_KEY"]
BASE_URL = "https://api.aethexai.com/api/v1"
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# Pick the first non-cloned English voice from the global catalog.
voices_resp = requests.get(f"{BASE_URL}/voices?language=english", headers=headers)
voices_resp.raise_for_status()
voices = [v for v in voices_resp.json() if not v.get("is_cloned")]
if not voices:
    raise SystemExit("No voices found. Check your API key and try again.")
voice_id = voices[0]["id"]

SYSTEM_PROMPT = """You are Maya, a customer service agent for First Capital Bank. You help
existing customers with general account questions: branch hours, fees,
how to dispute a charge, how to enroll in mobile banking, and what
documents you need for common requests.

Speak in short, clear sentences. Be warm but professional, not chatty.
When the caller's request is ambiguous, restate it before answering to
confirm you understood. If the caller sounds frustrated, slow down and
acknowledge it before moving on.

When you do not know an answer from the bank's knowledge base, say so
and offer to transfer to a human agent. Never invent a fee, policy, or
branch hour. Never ask for a full account number, social security
number, or PIN; those go through the secure online portal. Never
discuss another customer's account.

Tools you can use:
- end_call: invoke after the customer confirms they have what they
  need, or after a clear goodbye on either side.
- transfer_to_number: invoke for account-specific requests (fraud
  claim, lost card, large transfer, dispute escalation) or whenever
  the customer asks for a human.
"""

agent_resp = requests.post(f"{BASE_URL}/agents", headers=headers, json={
    "name": "First Capital Support",
    "system_prompt": SYSTEM_PROMPT,
    "first_message": "Hi, thanks for calling First Capital Bank. This is Maya. How can I help today?",
    "voice_id": voice_id,
    "language": "english",
})
agent_resp.raise_for_status()
agent = agent_resp.json()

print(f"Agent created: {agent['id']}")

3. Parlez à votre agent depuis un navigateur

Maintenant que vous avez un agent_id, le moyen le plus rapide de l'entendre parler est d'ouvrir une session WebRTC depuis un navigateur. Deux fichiers : un petit serveur Flask qui relaie les appels de signalisation (pour que votre clé API n'atteigne jamais le navigateur) et un client HTML monopage qui se connecte via celui-ci.Enregistrez les deux fichiers à côté de votre create_agent.py, définissez AGENT_ID avec l'identifiant que vous venez d'afficher, lancez python3 server.py, puis ouvrez http://localhost:5000 dans un navigateur. Autorisez l'accès au micro lorsque demandé.

Attention
macOS : Le port 5000 est utilisé par le récepteur AirPlay, qui répond à localhost:5000 par 403 Forbidden même si server.py indique qu'il a démarré. Si cela se produit, désactivez-le dans Réglages Système → Général → AirDrop et Handoff → Récepteur AirPlay, ou remplacez port=5000 dans server.py et l'URL SERVER dans client.html par 5001.
server.py
python
# Run with:
#   AETHEX_API_KEY=ae_live_... AGENT_ID=<from step 2> python3 server.py
#
# The browser cannot hold the API key, so the server proxies the
# two WebRTC signaling calls (/connect, /offer) to Aethex
# on its behalf.

import os
import requests
from flask import Flask, jsonify, request, send_from_directory

API_KEY = os.environ["AETHEX_API_KEY"]
AGENT_ID = os.environ["AGENT_ID"]
BASE_URL = "https://api.aethexai.com/api/v1"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

app = Flask(__name__)

@app.after_request
def add_cors(response):
    response.headers["Access-Control-Allow-Origin"] = "http://localhost:5000"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, PATCH, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type"
    return response

@app.route("/")
def index():
    return send_from_directory(os.path.dirname(__file__), "client.html")

@app.post("/session")
def start_session():
    r = requests.post(
        f"{BASE_URL}/conversation/connect",
        json={"agent_id": AGENT_ID},
        headers=HEADERS,
    )
    r.raise_for_status()
    return jsonify(r.json())  # {session_id, ice_config}

@app.post("/session/<sid>/offer")
def proxy_offer(sid):
    r = requests.post(
        f"{BASE_URL}/conversation/{sid}/offer",
        json=request.get_json(),
        headers=HEADERS,
    )
    return jsonify(r.json()), r.status_code

if __name__ == "__main__":
    app.run(port=5000)

Le client navigateur

La page HTML est servie par server.py sur /. Elle appelle /session pour démarrer un appel, ouvre une RTCPeerConnection, envoie une offre SDP et lit l'audio de l'agent quand il arrive. Total : ~40 lignes de JS vanilla, aucune étape de compilation.

Astuce
C'est le flux minimum viable. La page de concept WebRTC détaille la version production : ICE trickle, reconnexion, visualisation du niveau audio et la variante côté serveur sécurisée où le navigateur ne voit jamais l'identifiant de l'agent.
client.html
html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Aethex AI – Quickstart</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      background: #f5f5f7;
      color: #1d1d1f;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 100vh;
    }
    .card {
      background: #fff;
      border-radius: 16px;
      box-shadow: 0 2px 12px rgba(0,0,0,.08);
      padding: 2.5rem 2rem;
      width: 380px;
      text-align: center;
    }
    h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: .25rem; }
    .subtitle { font-size: .85rem; color: #86868b; margin-bottom: 1.5rem; }

    .badge {
      font-size: .75rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: .04em;
      padding: .25rem .75rem;
      border-radius: 999px;
      margin-bottom: 1.5rem;
    }
    .state-idle, .state-ended { background: #e8e8ed; color: #6e6e73; }
    .state-connecting         { background: #fff3cd; color: #856404; }
    .state-connected          { background: #d4edda; color: #155724; }
    .state-error              { background: #f8d7da; color: #721c24; }

    .btn {
      font-size: .95rem;
      font-weight: 500;
      border: none;
      border-radius: 10px;
      padding: .7rem 1.6rem;
      cursor: pointer;
    }
    .btn:disabled { opacity: .4; cursor: default; }
    .btn-start { background: #0071e3; color: #fff; }
    .btn-end   { background: #ff3b30; color: #fff; }

    .error-msg { margin-top: 1rem; font-size: .85rem; color: #ff3b30; }
  </style>
</head>
<body>
  <div class="card">
    <h1>Aethex Voice Agent</h1>
    <p class="subtitle">Quickstart demo</p>

    <div><span id="badge" class="badge state-idle">Idle</span></div>

    <button id="startBtn" class="btn btn-start">Start call</button>
    <button id="endBtn" class="btn btn-end" disabled>End call</button>

    <audio id="out" autoplay playsinline></audio>
    <p id="error" class="error-msg"></p>
  </div>

  <script>
    const SERVER = "http://localhost:5000"
    const badge = document.getElementById("badge")
    const startBtn = document.getElementById("startBtn")
    const endBtn = document.getElementById("endBtn")
    const audioEl = document.getElementById("out")
    const errorEl = document.getElementById("error")

    let pc = null
    let micStream = null
    let pingTimer = null

    function setState(name, label) {
      badge.className = "badge state-" + name
      badge.textContent = label || name
    }

    function stopLivenessPing() {
      if (pingTimer) {
        clearInterval(pingTimer)
        pingTimer = null
      }
    }

    function endCall(reason) {
      stopLivenessPing()
      if (micStream) { micStream.getTracks().forEach(t => t.stop()); micStream = null }
      if (pc) { pc.close(); pc = null }
      audioEl.srcObject = null
      endBtn.disabled = true
      startBtn.disabled = false
      if (reason !== "error") setState("ended", "Call ended")
    }

    startBtn.onclick = async () => {
      startBtn.disabled = true
      errorEl.textContent = ""

      try {
        setState("connecting", "Connecting...")

        const res = await fetch(`${SERVER}/session`, { method: "POST" })
        if (!res.ok) throw new Error(`Session failed (${res.status})`)
        const { session_id, ice_config } = await res.json()

        pc = new RTCPeerConnection(ice_config)
        const appChannel = pc.createDataChannel("aethex-app", { ordered: true })
        appChannel.onopen = () => {
          const sendPing = () => {
            if (appChannel.readyState === "open") appChannel.send(`ping:${Date.now()}`)
          }
          sendPing()
          pingTimer = setInterval(sendPing, 1000)
        }
        appChannel.onclose = stopLivenessPing
        appChannel.onerror = stopLivenessPing

        pc.onconnectionstatechange = () => {
          if (!pc) return
          if (pc.connectionState === "connected") setState("connected", "Connected")
          if (["failed", "closed"].includes(pc.connectionState)) endCall("ended")
        }

        pc.ontrack = (ev) => { audioEl.srcObject = ev.streams[0] }

        micStream = await navigator.mediaDevices.getUserMedia({ audio: true })
        micStream.getTracks().forEach(t => pc.addTrack(t, micStream))

        const offer = await pc.createOffer()
        await pc.setLocalDescription(offer)

        if (pc.iceGatheringState !== "complete") {
          const localPc = pc
          await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
              localPc.onicegatheringstatechange = null
              reject(new Error("ICE gathering timed out"))
            }, 10000)
            localPc.onicegatheringstatechange = () => {
              if (localPc.iceGatheringState === "complete") {
                clearTimeout(timeout)
                localPc.onicegatheringstatechange = null
                resolve()
              }
            }
          })
        }

        const { sdp, type } = pc.localDescription
        const answerRes = await fetch(`${SERVER}/session/${session_id}/offer`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ sdp, type }),
        })
        if (!answerRes.ok) throw new Error(`Offer failed (${answerRes.status})`)
        const answer = await answerRes.json()
        await pc.setRemoteDescription({ sdp: answer.sdp, type: "answer" })

        endBtn.disabled = false
      } catch (err) {
        errorEl.textContent = err.message
        setState("error", "Error")
        endCall("error")
      }
    }

    endBtn.onclick = () => endCall("ended")
  </script>
</body>
</html>

Étapes suivantes

Maintenant que vous avez un agent fonctionnel, voici les ajouts les plus courants.

  1. Appeler de vrais numéros de téléphone

    Ajoutez un numéro de trois façons : connectez un numéro Twilio géré, apportez votre propre opérateur/PBX via SIP, ou connectez Telnyx et nous provisionnons le trunk pour vous. Les numéros Twilio et SIP s'enregistrent en une étape ; les numéros Telnyx sont enregistrés lors du provisionnement. Déclenchez des appels sortants depuis n'importe quelle ligne enregistrée — les lignes SIP en propre nécessitent en plus un trunk sortant créé via /sip-trunks/onboard ; un numéro rattaché à un trunk entrant uniquement ne peut pas émettre d'appels. Guide : Numéros de téléphone.

    python
    # After registering a number on the dashboard:
    call_resp = requests.post(f"{BASE_URL}/calls/trigger", headers=headers, json={
        "agent_id": agent["id"],
        "to_number": "+14155551234",   # the phone you want to dial
        "from_number": "+18885551234", # your registered Aethex number
        "dynamic_variables": {         # overrides agent defaults; required only for placeholders with no default
            "customer_name": "Alice",
        },
    })
    call_resp.raise_for_status()
    print(f"Call started: {call_resp.json()['id']}")
  2. Choisir une voix différente

    Parcourez GET /voices pour voir le catalogue, puis définissez voice_id sur votre agent. Les voix varient selon la langue et le style.

  3. Ajouter des outils et des connaissances

    Connectez des outils de function-calling pour que l'agent puisse chercher des données ou déclencher des actions, et téléchargez des documents de base de connaissances (PDF/DOCX/TXT/MD) pour des réponses enrichies par RAG.

  4. Intégrer la voix dans votre propre application

    Utilisez directement l'API WebRTC pour intégrer l'agent dans un navigateur ou une application mobile que vous contrôlez, avec l'audio complet en entrée et sortie.