Skip to content

Commit cdd0080

Browse files
authored
Merge pull request #23 from basicmachines-co/claw/conversation-schema
feat: Add Conversation schema, seeded on startup
2 parents 9ab86e1 + 05fbaa1 commit cdd0080

File tree

5 files changed

+110
-37
lines changed

5 files changed

+110
-37
lines changed

benchmark/convert-locomo.ts

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,24 @@ function parseDateTime(dateStr: string): { date: string; time: string } | null {
7878
)
7979
if (!match) return null
8080

81-
let [, hour, min, ampm, day, month, year] = match
81+
const [, hour, min, ampm, day, month, year] = match
8282
let h = Number.parseInt(hour)
8383
if (ampm.toLowerCase() === "pm" && h !== 12) h += 12
8484
if (ampm.toLowerCase() === "am" && h === 12) h = 0
8585

8686
const months: Record<string, string> = {
87-
January: "01", February: "02", March: "03", April: "04",
88-
May: "05", June: "06", July: "07", August: "08",
89-
September: "09", October: "10", November: "11", December: "12",
87+
January: "01",
88+
February: "02",
89+
March: "03",
90+
April: "04",
91+
May: "05",
92+
June: "06",
93+
July: "07",
94+
August: "08",
95+
September: "09",
96+
October: "10",
97+
November: "11",
98+
December: "12",
9099
}
91100

92101
const m = months[month]
@@ -138,7 +147,10 @@ type: Person
138147
- [role] Conversation participant
139148
- [relationship] Regularly chats with ${speakerB}
140149
`
141-
files.set(`people/${speakerA.toLowerCase().replace(/\s+/g, "-")}.md`, speakerANote)
150+
files.set(
151+
`people/${speakerA.toLowerCase().replace(/\s+/g, "-")}.md`,
152+
speakerANote,
153+
)
142154

143155
const speakerBNote = `---
144156
title: ${speakerB}
@@ -151,16 +163,19 @@ type: Person
151163
- [role] Conversation participant
152164
- [relationship] Regularly chats with ${speakerA}
153165
`
154-
files.set(`people/${speakerB.toLowerCase().replace(/\s+/g, "-")}.md`, speakerBNote)
166+
files.set(
167+
`people/${speakerB.toLowerCase().replace(/\s+/g, "-")}.md`,
168+
speakerBNote,
169+
)
155170

156171
// Build a MEMORY.md with key facts that accumulate
157-
let memoryLines: string[] = [
158-
`# Long-Term Memory`,
172+
const memoryLines: string[] = [
173+
"# Long-Term Memory",
159174
"",
160-
`## People`,
175+
"## People",
161176
`- ${speakerA} and ${speakerB} are close friends who chat regularly`,
162177
"",
163-
`## Key Events`,
178+
"## Key Events",
164179
]
165180

166181
// Convert each session to a dated note
@@ -170,7 +185,8 @@ type: Person
170185
const dateTimeStr = c[`${sessionKey}_date_time`]
171186
const parsed = dateTimeStr ? parseDateTime(dateTimeStr) : null
172187

173-
const date = parsed?.date || `2023-01-${String(sessionNum).padStart(2, "0")}`
188+
const date =
189+
parsed?.date || `2023-01-${String(sessionNum).padStart(2, "0")}`
174190
const time = parsed?.time || "12:00"
175191

176192
// Get session summary and observations if available
@@ -184,7 +200,8 @@ type: Person
184200
if (Array.isArray(obs)) {
185201
for (const item of obs) {
186202
const text = Array.isArray(item) ? item[0] : item
187-
if (typeof text === "string") lines.push(`- [${speaker.toLowerCase()}] ${text}`)
203+
if (typeof text === "string")
204+
lines.push(`- [${speaker.toLowerCase()}] ${text}`)
188205
}
189206
}
190207
}
@@ -213,28 +230,29 @@ date: ${date}
213230
}
214231

215232
// Add conversation
216-
content += `## Conversation\n`
233+
content += "## Conversation\n"
217234
for (const turn of turns) {
218235
const text = turn.text.replace(/\n/g, "\n> ")
219236
content += `**${turn.speaker}:** ${text}\n\n`
220237
}
221238

222239
// Add relations
223-
content += `## Relations\n`
240+
content += "## Relations\n"
224241
content += `- mentions [[${speakerA}]]\n`
225242
content += `- mentions [[${speakerB}]]\n`
226243

227244
// Add to memory summary
228245
if (observation) {
229-
const firstObs = observation.split("\n")[0]?.replace(/^- \[\w+\] /, "") || ""
246+
const firstObs =
247+
observation.split("\n")[0]?.replace(/^- \[\w+\] /, "") || ""
230248
if (firstObs) memoryLines.push(`- [${date}] ${firstObs}`)
231249
}
232250

233251
files.set(`conversations/${date}-session-${sessionNum}.md`, content)
234252
}
235253

236254
// Write MEMORY.md
237-
files.set("MEMORY.md", memoryLines.join("\n") + "\n")
255+
files.set("MEMORY.md", `${memoryLines.join("\n")}\n`)
238256

239257
// Convert QA to benchmark queries
240258
const queries: BenchmarkQuery[] = []
@@ -253,7 +271,8 @@ date: ${date}
253271
// Find the session's date
254272
const dateTimeStr = c[`session_${sessionNum}_date_time`]
255273
const parsed = dateTimeStr ? parseDateTime(dateTimeStr) : null
256-
const date = parsed?.date || `2023-01-${String(sessionNum).padStart(2, "0")}`
274+
const date =
275+
parsed?.date || `2023-01-${String(sessionNum).padStart(2, "0")}`
257276
groundTruth.add(`conversations/${date}-session-${sessionNum}.md`)
258277
}
259278

@@ -266,8 +285,14 @@ date: ${date}
266285
query: qa.question,
267286
category,
268287
ground_truth: [...groundTruth],
269-
expected_content: isAdversarial ? undefined : answer.length < 100 ? answer : undefined,
270-
note: isAdversarial ? `Adversarial: correct answer is "${answer}"` : undefined,
288+
expected_content: isAdversarial
289+
? undefined
290+
: answer.length < 100
291+
? answer
292+
: undefined,
293+
note: isAdversarial
294+
? `Adversarial: correct answer is "${answer}"`
295+
: undefined,
271296
})
272297
}
273298

@@ -303,7 +328,9 @@ async function main() {
303328
const convDir = `corpus-locomo/conv-${idx}`
304329
const outDir = resolve(BENCHMARK_DIR, convDir)
305330

306-
console.log(`\nConverting conversation ${idx} (${conv.conversation.speaker_a} & ${conv.conversation.speaker_b})...`)
331+
console.log(
332+
`\nConverting conversation ${idx} (${conv.conversation.speaker_a} & ${conv.conversation.speaker_b})...`,
333+
)
307334

308335
const { files, queries } = convertConversation(conv, idx)
309336

@@ -332,8 +359,10 @@ async function main() {
332359
}
333360
}
334361

335-
console.log(`\n✅ Total: ${totalFiles} files, ${totalQueries} queries across ${indices.length} conversations`)
336-
console.log(` Output: benchmark/corpus-locomo/`)
362+
console.log(
363+
`\n✅ Total: ${totalFiles} files, ${totalQueries} queries across ${indices.length} conversations`,
364+
)
365+
console.log(" Output: benchmark/corpus-locomo/")
337366
}
338367

339368
main().catch((err) => {

benchmark/run.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,15 @@ const RESULTS_DIR = resolve(BENCHMARK_DIR, "results")
8787
const CORPUS_SIZE =
8888
process.argv.find((a) => a.startsWith("--corpus="))?.split("=")[1] || "small"
8989
const BM_PROJECT =
90-
process.argv.find((a) => a.startsWith("--project="))?.split("=")[1] || "benchmark"
90+
process.argv.find((a) => a.startsWith("--project="))?.split("=")[1] ||
91+
"benchmark"
9192
const QUERIES_PATH =
9293
process.argv.find((a) => a.startsWith("--queries="))?.split("=")[1] ||
9394
resolve(BENCHMARK_DIR, "queries.json")
94-
const QUERY_LIMIT = Number.parseInt(
95-
process.argv.find((a) => a.startsWith("--limit="))?.split("=")[1] || "0",
96-
) || 0
95+
const QUERY_LIMIT =
96+
Number.parseInt(
97+
process.argv.find((a) => a.startsWith("--limit="))?.split("=")[1] || "0",
98+
) || 0
9799

98100
// ---------------------------------------------------------------------------
99101
// MCP Client

index.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { buildCaptureHandler } from "./hooks/capture.ts"
1515
import { buildRecallHandler } from "./hooks/recall.ts"
1616
import { initLogger, log } from "./logger.ts"
17+
import { CONVERSATION_SCHEMA_CONTENT } from "./schema/conversation-schema.ts"
1718
import { TASK_SCHEMA_CONTENT } from "./schema/task-schema.ts"
1819
import { registerContextTool } from "./tools/build-context.ts"
1920
import { registerDeleteTool } from "./tools/delete-note.ts"
@@ -103,7 +104,9 @@ export default {
103104
'uv tool install "basic-memory @ git+https://github.com/basicmachines-co/basic-memory.git@main" --force',
104105
{ encoding: "utf-8", timeout: 120_000, stdio: "pipe" },
105106
)
106-
log.info(`basic-memory installed: ${result.trim().split("\n").pop()}`)
107+
log.info(
108+
`basic-memory installed: ${result.trim().split("\n").pop()}`,
109+
)
107110
// Verify it worked
108111
try {
109112
execSync(`command -v ${bmBin}`, { stdio: "ignore" })
@@ -113,7 +116,7 @@ export default {
113116
"bm installed but not found on PATH. You may need to add uv's bin directory to your PATH (typically ~/.local/bin).",
114117
)
115118
}
116-
} catch (uvErr) {
119+
} catch (_uvErr) {
117120
log.error(
118121
"Cannot auto-install basic-memory: uv not found. " +
119122
"Install uv first (brew install uv, or curl -LsSf https://astral.sh/uv/install.sh | sh), " +
@@ -130,16 +133,21 @@ export default {
130133
await client.ensureProject(projectPath)
131134
log.debug(`project "${cfg.project}" at ${projectPath}`)
132135

133-
// Seed Task schema if not already present
134-
try {
135-
await client.readNote("schema/Task")
136-
log.debug("Task schema already exists, skipping seed")
137-
} catch {
136+
// Seed schemas if not already present
137+
for (const [name, content] of [
138+
["Task", TASK_SCHEMA_CONTENT],
139+
["Conversation", CONVERSATION_SCHEMA_CONTENT],
140+
] as const) {
138141
try {
139-
await client.writeNote("Task", TASK_SCHEMA_CONTENT, "schema")
140-
log.debug("seeded Task schema note")
141-
} catch (err) {
142-
log.debug("Task schema seed failed (non-fatal)", err)
142+
await client.readNote(`schema/${name}`)
143+
log.debug(`${name} schema already exists, skipping seed`)
144+
} catch {
145+
try {
146+
await client.writeNote(name, content, "schema")
147+
log.debug(`seeded ${name} schema note`)
148+
} catch (err) {
149+
log.debug(`${name} schema seed failed (non-fatal)`, err)
150+
}
143151
}
144152
}
145153

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"tools/write-note.ts",
3434
"types/openclaw.d.ts",
3535
"schema/task-schema.ts",
36+
"schema/conversation-schema.ts",
3637
"skills/",
3738
"scripts/setup-bm.sh",
3839
"openclaw.plugin.json",

schema/conversation-schema.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Canonical Conversation schema note content, seeded into new projects on first startup.
3+
* If the schema already exists (user may have customized it), seeding is skipped.
4+
*/
5+
export const CONVERSATION_SCHEMA_CONTENT = `---
6+
title: Conversation
7+
type: schema
8+
entity: Conversation
9+
version: 1
10+
schema:
11+
date: "string, ISO date YYYY-MM-DD"
12+
session_id?: "string, unique session identifier"
13+
channel?: "string, where the conversation happened (webchat, telegram, discord, etc)"
14+
participants?: "array, who was in the conversation"
15+
topic?: "string, brief description of main topic"
16+
summary?: "string, one-paragraph summary of key points"
17+
key_decisions?: "array, decisions made during conversation"
18+
action_items?: "array, things to do as a result"
19+
settings:
20+
validation: warn
21+
---
22+
23+
# Conversation
24+
25+
A record of a conversation session between the agent and user(s).
26+
27+
## Observations
28+
- [convention] Conversation files live in memory/conversations/ with format conversations-YYYY-MM-DD.md
29+
- [convention] Messages are appended as the conversation progresses
30+
- [convention] Summary and key_decisions populated at session end or by memory-reflect
31+
- [convention] Skip routine greetings, heartbeat acks, and tool call details
32+
- [convention] Focus on decisions, actions taken, and key context needed for continuity
33+
`

0 commit comments

Comments
 (0)