ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
const $ = (selector) => document.querySelector(selector);
const state = {
callId: null,
plan: null,
pc: null,
dc: null,
stream: null,
statsTimer: null,
remoteAudioTrack: false,
localAudioTrack: false,
lastStats: null,
audioCommitted: false,
};
function mode() {
return document.querySelector('input[name="mode"]:checked').value;
}
function setStatus(value) {
$('#status').textContent = value;
document.body.dataset.status = value;
}
function appendTurn(speaker, text) {
if (!text) return;
const p = document.createElement('p');
p.className = 'turn';
const b = document.createElement('b');
b.textContent = speaker === 'agent' ? 'Agent: ' : 'You: ';
p.append(b, document.createTextNode(text));
$('#transcript').appendChild(p);
$('#transcript').scrollTop = $('#transcript').scrollHeight;
}
async function jsonFetch(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {'Content-Type': 'application/json', ...(options.headers || {})},
});
const data = await response.json();
if (!response.ok) throw new Error(data.detail || `HTTP ${response.status}`);
return data;
}
async function saveEvent(event) {
if (!state.callId) return;
try {
await jsonFetch(`/api/calls/${state.callId}/events`, {
method: 'POST',
body: JSON.stringify({event}),
});
} catch (error) {
console.warn('event receipt failed', error);
}
}
function sendControl(event) {
if (!state.dc || state.dc.readyState !== 'open') throw new Error('data channel is not open');
state.dc.send(JSON.stringify(event));
}
async function publishReady() {
if (!state.pc) return;
const open = state.dc?.readyState === 'open';
const connected = ['connected', 'completed'].includes(state.pc.iceConnectionState);
$('#transport').textContent = `WebRTC · ICE ${state.pc.iceConnectionState} · data ${state.dc?.readyState || 'new'}`;
if (open && connected) setStatus('connected · listening');
await saveEvent({
type: 'rtc.ready',
ice_connection_state: state.pc.iceConnectionState,
data_channel_open: open,
local_audio_track: state.localAudioTrack,
remote_audio_track: state.remoteAudioTrack,
});
}
async function collectStats() {
if (!state.pc) return null;
const totals = {
type: 'rtc.stats',
ice_connection_state: state.pc.iceConnectionState,
inbound_packets: 0,
inbound_bytes: 0,
outbound_packets: 0,
outbound_bytes: 0,
};
const reports = await state.pc.getStats();
reports.forEach((report) => {
const kind = report.kind || report.mediaType;
if (kind !== 'audio') return;
if (report.type === 'inbound-rtp' && !report.isRemote) {
totals.inbound_packets += report.packetsReceived || 0;
totals.inbound_bytes += report.bytesReceived || 0;
}
if (report.type === 'outbound-rtp' && !report.isRemote) {
totals.outbound_packets += report.packetsSent || 0;
totals.outbound_bytes += report.bytesSent || 0;
}
});
state.lastStats = totals;
await saveEvent(totals);
return totals;
}
async function handleServerEvent(event) {
if (event.type === 'agent.caption') appendTurn('agent', event.text);
if (event.type === 'user.caption') appendTurn('user', event.text);
if (event.type === 'tool.result') {
setStatus('task completed · Agent audio playing');
$('#evidence').textContent = JSON.stringify(event, null, 2);
}
if (event.type === 'error') {
setStatus('error');
$('#evidence').textContent = JSON.stringify(event.error || event, null, 2);
}
}
function createRequest() {
if (mode() === 'react') return {mode: 'react', task: $('#task').value};
return {
mode: 'direct',
callee_name: $('#callee-name').value,
goal: $('#goal').value,
context: $('#context').value,
instructions: $('#instructions').value,
};
}
async function startCall() {
$('#start').disabled = true;
setStatus('real LLM planning');
try {
const created = await jsonFetch('/api/calls', {method: 'POST', body: JSON.stringify(createRequest())});
state.callId = created.call_id;
state.plan = created.plan;
state.audioCommitted = false;
$('#call-id').textContent = state.callId;
$('#evidence').textContent = JSON.stringify({plan: state.plan}, null, 2);
const pc = new RTCPeerConnection();
state.pc = pc;
const audio = $('#remote-audio');
pc.ontrack = async (event) => {
audio.srcObject = event.streams[0];
state.remoteAudioTrack = event.track.kind === 'audio';
try { await audio.play(); } catch (error) { console.warn('autoplay pending', error); }
await publishReady();
};
pc.oniceconnectionstatechange = publishReady;
pc.onconnectionstatechange = publishReady;
setStatus('requesting microphone');
state.stream = await navigator.mediaDevices.getUserMedia({audio: true, video: false});
const track = state.stream.getAudioTracks()[0];
if (!track) throw new Error('no microphone audio track was returned');
state.localAudioTrack = true;
pc.addTrack(track, state.stream);
const dc = pc.createDataChannel('accessibility-and-control');
state.dc = dc;
dc.onmessage = (message) => handleServerEvent(JSON.parse(message.data)).catch(console.error);
dc.onclose = publishReady;
dc.onopen = async () => {
await publishReady();
sendControl({type: 'client.ready'});
$('#commit-audio').disabled = false;
};
setStatus('negotiating WebRTC');
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answerResponse = await fetch(`/api/calls/${state.callId}/session`, {
method: 'POST',
headers: {'Content-Type': 'application/sdp'},
body: offer.sdp,
});
const answerSdp = await answerResponse.text();
if (!answerResponse.ok) throw new Error(answerSdp);
await pc.setRemoteDescription({type: 'answer', sdp: answerSdp});
state.statsTimer = setInterval(() => collectStats().catch(console.warn), 750);
$('#hangup').disabled = false;
} catch (error) {
setStatus('error');
$('#evidence').textContent = String(error);
$('#start').disabled = false;
throw error;
}
}
async function commitAudio() {
if (state.audioCommitted) throw new Error('microphone audio was already committed');
state.audioCommitted = true;
$('#commit-audio').disabled = true;
setStatus('Whisper ASR · real LLM dialogue');
sendControl({type: 'client.audio.commit'});
}
async function hangup(reason = 'user_hangup') {
if (!state.callId) return null;
if (state.statsTimer) clearInterval(state.statsTimer);
await collectStats();
state.stream?.getTracks().forEach((track) => track.stop());
state.dc?.close();
state.pc?.close();
const record = await jsonFetch(`/api/calls/${state.callId}/finish`, {
method: 'POST',
body: JSON.stringify({reason}),
});
setStatus(record.acceptance.passed ? 'completed' : 'ended');
$('#evidence').textContent = JSON.stringify(record, null, 2);
$('#hangup').disabled = true;
$('#commit-audio').disabled = true;
return record;
}
document.querySelectorAll('input[name="mode"]').forEach((radio) => {
radio.addEventListener('change', () => {
const react = mode() === 'react';
$('#react-fields').hidden = !react;
$('#direct-fields').hidden = react;
});
});
$('#start').addEventListener('click', () => startCall().catch(console.error));
$('#commit-audio').addEventListener('click', () => commitAudio().catch(console.error));
$('#hangup').addEventListener('click', () => hangup().catch(console.error));
window.exp92 = {state, startCall, commitAudio, hangup, collectStats};
const params = new URLSearchParams(window.location.search);
if (params.get('mode') === 'direct') {
document.querySelector('input[name="mode"][value="direct"]').click();
}
const queryFields = {
task: '#task',
callee_name: '#callee-name',
goal: '#goal',
context: '#context',
instructions: '#instructions',
};
Object.entries(queryFields).forEach(([key, selector]) => {
if (params.has(key)) $(selector).value = params.get(key);
});
+70
View File
@@ -0,0 +1,70 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Phone Agent add-on · WebRTC Call Agent</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<main>
<header>
<p class="eyebrow">AI Agent Book · Phone Agent add-on</p>
<h1>A voice agent that calls you in the browser</h1>
<p class="lede">No phone number or PSTN account is needed. You explicitly join this local WebRTC session and can hang up at any time.</p>
</header>
<section class="card setup">
<div class="mode-row">
<label><input type="radio" name="mode" value="react" checked> ReAct plan</label>
<label><input type="radio" name="mode" value="direct"> Direct parameters</label>
</div>
<div id="react-fields">
<label for="task">Natural-language task</label>
<textarea id="task" rows="4">Call me to arrange a dental checkup for Jane Doe. I did not include a time, so ask me for it, repeat the details, and save the confirmation I provide.</textarea>
</div>
<div id="direct-fields" hidden>
<label for="callee-name">Participant name</label>
<input id="callee-name" value="Jane Doe">
<label for="goal">Goal</label>
<textarea id="goal" rows="2">Collect and confirm Jane Doe's preferred dental-checkup time.</textarea>
<label for="context">Context</label>
<textarea id="context" rows="2">Tuesday afternoon from 2pm to 4pm is available.</textarea>
<label for="instructions">Instructions</label>
<textarea id="instructions" rows="3">Ask for one exact time, repeat it, request confirmation, and save the confirmed time and confirmation number with complete_task.</textarea>
</div>
<div class="actions">
<button id="start" type="button">Join call and enable microphone</button>
<button id="hangup" class="secondary" type="button" disabled>Hang up</button>
</div>
<p class="privacy">Provider credentials stay on the local server. The browser receives only the SDP answer; no credential is embedded in this page.</p>
</section>
<section class="card status-card">
<div><span class="label">Status</span><strong id="status">idle</strong></div>
<div><span class="label">Call ID</span><code id="call-id"></code></div>
<div><span class="label">Transport</span><span id="transport">WebRTC · not connected</span></div>
</section>
<section class="card conversation">
<h2>Audio transcript captions</h2>
<div id="transcript" aria-live="polite"></div>
<div class="actions">
<button id="commit-audio" class="secondary" type="button" disabled>Finish speaking</button>
</div>
<p class="privacy">Speak into the microphone, then choose “Finish speaking.” User meaning comes only from server ASR over the microphone RTP track. The data channel carries this control event and accessibility captions; it never supplies the canonical user transcript.</p>
</section>
<section class="card evidence">
<h2>Session evidence</h2>
<pre id="evidence">Start a call to see the negotiated local aiortc transport record.</pre>
</section>
<audio id="remote-audio" autoplay></audio>
</main>
<script src="/static/app.js" defer></script>
</body>
</html>
+41
View File
@@ -0,0 +1,41 @@
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #14221d;
background: #edf3ef;
}
* { box-sizing: border-box; }
body { margin: 0; }
main { width: min(880px, calc(100% - 32px)); margin: 48px auto 72px; }
header { margin-bottom: 28px; }
h1 { margin: 6px 0 12px; font-size: clamp(2rem, 6vw, 4.2rem); line-height: 0.98; letter-spacing: -0.045em; }
h2 { margin-top: 0; font-size: 1.15rem; }
.eyebrow, .label { color: #356450; text-transform: uppercase; letter-spacing: 0.09em; font-size: 0.76rem; font-weight: 750; }
.lede { max-width: 700px; color: #41534b; font-size: 1.08rem; line-height: 1.55; }
.card { background: #fff; border: 1px solid #cad8d0; border-radius: 18px; padding: 22px; margin: 14px 0; box-shadow: 0 12px 32px rgba(29, 67, 50, 0.06); }
.mode-row, .actions, .text-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.mode-row { margin-bottom: 18px; }
label { display: block; margin: 12px 0 6px; font-weight: 650; }
input, textarea, button { font: inherit; }
input:not([type="radio"]), textarea { width: 100%; border: 1px solid #aebfb6; border-radius: 10px; padding: 11px 12px; background: #fbfdfc; color: inherit; }
textarea { resize: vertical; }
button { border: 0; border-radius: 999px; padding: 11px 18px; background: #176b49; color: white; font-weight: 720; cursor: pointer; }
button.secondary { background: #dde8e2; color: #254537; }
button:disabled { opacity: 0.45; cursor: not-allowed; }
.actions { margin-top: 20px; }
.privacy { margin-bottom: 0; color: #68776f; font-size: 0.86rem; }
.status-card { display: grid; grid-template-columns: 0.7fr 1fr 1.2fr; gap: 16px; }
.status-card div { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
code { overflow-wrap: anywhere; }
#transcript { min-height: 110px; max-height: 330px; overflow: auto; padding: 12px; background: #f3f7f4; border-radius: 12px; }
.turn { margin: 0 0 10px; line-height: 1.45; }
.turn b { color: #176b49; }
.text-row { flex-wrap: nowrap; }
.text-row input { flex: 1; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; color: #31463d; font-size: 0.82rem; }
@media (max-width: 680px) {
main { margin-top: 24px; }
.status-card { grid-template-columns: 1fr; }
.text-row { flex-wrap: wrap; }
}