forked from open-multi-agent/open-multi-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmeeting-summarizer.ts
More file actions
284 lines (234 loc) · 9.57 KB
/
Copy pathmeeting-summarizer.ts
File metadata and controls
284 lines (234 loc) · 9.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
/**
* Meeting Summarizer (Parallel Post-Processing)
*
* Demonstrates:
* - Fan-out of three specialized agents on the same meeting transcript
* - Structured output (Zod schemas) for action items and sentiment
* - Parallel timing check: wall time vs sum of per-agent durations
* - Aggregator merges into a single Markdown report
*
* Run:
* npx tsx examples/patterns/meeting-summarizer.ts
*
* Prerequisites:
* ANTHROPIC_API_KEY env var must be set.
*/
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { z } from 'zod'
import { Agent, AgentPool, ToolRegistry, ToolExecutor, registerBuiltInTools } from '../../src/index.js'
import type { AgentConfig, AgentRunResult } from '../../src/types.js'
// ---------------------------------------------------------------------------
// Load the transcript fixture
// ---------------------------------------------------------------------------
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const TRANSCRIPT = readFileSync(
path.join(__dirname, '../fixtures/meeting-transcript.txt'),
'utf-8',
)
// ---------------------------------------------------------------------------
// Zod schemas for structured agents
// ---------------------------------------------------------------------------
const ActionItemList = z.object({
items: z.array(
z.object({
task: z.string().describe('The action to be taken'),
owner: z.string().describe('Name of the person responsible'),
due_date: z.string().optional().describe('ISO date or human-readable due date if mentioned'),
}),
),
})
type ActionItemList = z.infer<typeof ActionItemList>
const SentimentReport = z.object({
participants: z.array(
z.object({
participant: z.string().describe('Name as it appears in the transcript'),
tone: z.enum(['positive', 'neutral', 'negative', 'mixed']),
evidence: z.string().describe('Direct quote or brief paraphrase supporting the tone'),
}),
),
})
type SentimentReport = z.infer<typeof SentimentReport>
// ---------------------------------------------------------------------------
// Agent configs
// ---------------------------------------------------------------------------
const summaryConfig: AgentConfig = {
name: 'summary',
model: 'claude-sonnet-4-6',
systemPrompt: `You are a meeting note-taker. Given a transcript, produce a
three-paragraph summary:
1. What was discussed (the agenda).
2. Decisions made.
3. Notable context or risk the team should remember.
Plain prose. No bullet points. 200-300 words total.`,
maxTurns: 1,
temperature: 0.3,
}
const actionItemsConfig: AgentConfig = {
name: 'action-items',
model: 'claude-sonnet-4-6',
systemPrompt: `You extract action items from meeting transcripts. An action
item is a concrete task with a clear owner. Skip vague intentions ("we should
think about X"). Include due dates only when the speaker named one explicitly.
Return JSON matching the schema.`,
maxTurns: 1,
temperature: 0.1,
outputSchema: ActionItemList,
}
const sentimentConfig: AgentConfig = {
name: 'sentiment',
model: 'claude-sonnet-4-6',
systemPrompt: `You analyze the tone of each participant in a meeting. For
every named speaker, classify their overall tone as positive, neutral,
negative, or mixed, and include one short quote or paraphrase as evidence.
Return JSON matching the schema.`,
maxTurns: 1,
temperature: 0.2,
outputSchema: SentimentReport,
}
const aggregatorConfig: AgentConfig = {
name: 'aggregator',
model: 'claude-sonnet-4-6',
systemPrompt: `You are a report writer. You receive three pre-computed
analyses of the same meeting: a summary, an action-item list, and a sentiment
report. Your job is to merge them into a single Markdown report.
Output structure — use exactly these four H2 headings, in order:
## Summary
## Action Items
## Sentiment
## Next Steps
Under "Action Items" render a Markdown table with columns: Task, Owner, Due.
Under "Sentiment" render one bullet per participant.
Under "Next Steps" synthesize 3-5 concrete follow-ups based on the other
sections. Do not invent action items that are not grounded in the other data.`,
maxTurns: 1,
temperature: 0.3,
}
// ---------------------------------------------------------------------------
// Build agents
// ---------------------------------------------------------------------------
function buildAgent(config: AgentConfig): Agent {
const registry = new ToolRegistry()
registerBuiltInTools(registry)
const executor = new ToolExecutor(registry)
return new Agent(config, registry, executor)
}
const summary = buildAgent(summaryConfig)
const actionItems = buildAgent(actionItemsConfig)
const sentiment = buildAgent(sentimentConfig)
const aggregator = buildAgent(aggregatorConfig)
const pool = new AgentPool(3) // three specialists can run concurrently
pool.add(summary)
pool.add(actionItems)
pool.add(sentiment)
pool.add(aggregator)
console.log('Meeting Summarizer (Parallel Post-Processing)')
console.log('='.repeat(60))
console.log(`\nTranscript: ${TRANSCRIPT.split('\n')[0]}`)
console.log(`Length: ${TRANSCRIPT.split(/\s+/).length} words\n`)
// ---------------------------------------------------------------------------
// Step 1: Parallel fan-out with per-agent timing
// ---------------------------------------------------------------------------
console.log('[Step 1] Running 3 agents in parallel...\n')
const specialists = ['summary', 'action-items', 'sentiment'] as const
// Kick off all three concurrently and record each one's own wall duration.
// Sum-of-per-agent beats a separate serial pass: half the LLM cost, and the
// sum is the work parallelism saved.
const parallelStart = performance.now()
const timed = await Promise.all(
specialists.map(async (name) => {
const t = performance.now()
const result = await pool.run(name, TRANSCRIPT)
return { name, result, durationMs: performance.now() - t }
}),
)
const parallelElapsed = performance.now() - parallelStart
const byName = new Map<string, AgentRunResult>()
const serialSum = timed.reduce((acc, r) => {
byName.set(r.name, r.result)
return acc + r.durationMs
}, 0)
for (const { name, result, durationMs } of timed) {
const status = result.success ? 'OK' : 'FAILED'
console.log(
` ${name.padEnd(14)} [${status}] — ${Math.round(durationMs)}ms, ${result.tokenUsage.output_tokens} out tokens`,
)
}
console.log()
for (const { name, result } of timed) {
if (!result.success) {
console.error(`Specialist '${name}' failed: ${result.output}`)
process.exit(1)
}
}
const actionData = byName.get('action-items')!.structured as ActionItemList | undefined
const sentimentData = byName.get('sentiment')!.structured as SentimentReport | undefined
if (!actionData || !sentimentData) {
console.error('Structured output missing: action-items or sentiment failed schema validation')
process.exit(1)
}
// ---------------------------------------------------------------------------
// Step 2: Parallelism assertion
// ---------------------------------------------------------------------------
console.log('[Step 2] Parallelism check')
console.log(` Parallel wall time: ${Math.round(parallelElapsed)}ms`)
console.log(` Serial sum (per-agent): ${Math.round(serialSum)}ms`)
console.log(` Speedup: ${(serialSum / parallelElapsed).toFixed(2)}x\n`)
if (parallelElapsed >= serialSum * 0.7) {
console.error(
`ASSERTION FAILED: parallel wall time (${Math.round(parallelElapsed)}ms) is not ` +
`less than 70% of serial sum (${Math.round(serialSum)}ms). Expected substantial ` +
`speedup from fan-out.`,
)
process.exit(1)
}
// ---------------------------------------------------------------------------
// Step 3: Aggregate into Markdown report
// ---------------------------------------------------------------------------
console.log('[Step 3] Aggregating into Markdown report...\n')
const aggregatorPrompt = `Merge the three analyses below into a single Markdown report.
--- SUMMARY (prose) ---
${byName.get('summary')!.output}
--- ACTION ITEMS (JSON) ---
${JSON.stringify(actionData, null, 2)}
--- SENTIMENT (JSON) ---
${JSON.stringify(sentimentData, null, 2)}
Produce the Markdown report per the system instructions.`
const reportResult = await pool.run('aggregator', aggregatorPrompt)
if (!reportResult.success) {
console.error('Aggregator failed:', reportResult.output)
process.exit(1)
}
// ---------------------------------------------------------------------------
// Final output
// ---------------------------------------------------------------------------
console.log('='.repeat(60))
console.log('MEETING REPORT')
console.log('='.repeat(60))
console.log()
console.log(reportResult.output)
console.log()
console.log('-'.repeat(60))
// ---------------------------------------------------------------------------
// Token usage summary
// ---------------------------------------------------------------------------
console.log('\nToken Usage Summary:')
console.log('-'.repeat(60))
let totalInput = 0
let totalOutput = 0
for (const { name, result } of timed) {
totalInput += result.tokenUsage.input_tokens
totalOutput += result.tokenUsage.output_tokens
console.log(
` ${name.padEnd(14)} — input: ${result.tokenUsage.input_tokens}, output: ${result.tokenUsage.output_tokens}`,
)
}
totalInput += reportResult.tokenUsage.input_tokens
totalOutput += reportResult.tokenUsage.output_tokens
console.log(
` ${'aggregator'.padEnd(14)} — input: ${reportResult.tokenUsage.input_tokens}, output: ${reportResult.tokenUsage.output_tokens}`,
)
console.log('-'.repeat(60))
console.log(` ${'TOTAL'.padEnd(14)} — input: ${totalInput}, output: ${totalOutput}`)
console.log('\nDone.')