Skip to content

Commit c3ec5b3

Browse files
feat: add validation framework with conversation logger
Adds the /validate skill, conversation logging, and associated tooling. **Validation framework** - Add `src/skill/validate/SKILL.md` — 7-step validation workflow: steps 1–5 fetch and analyse traces, step 6 generates an HTML dashboard, step 7 categorises groundedness failures - Add `src/skill/validate/batch_validate.py` — calls Altimate backend SSE endpoints (`/validate`, `/validate/session`, `/validate/date-range`) and writes structured JSON results; hardcoded prod URL and token, no dotenv dependency - Embed both files at build time via `ALTIMATE_VALIDATE_SKILL_MD` and `ALTIMATE_VALIDATE_BATCH_PY` defines in `build.ts` - Replace LLM-based intent classifier with deterministic `/validate` prefix check via `resolvePrompt` helper in `session.ts`, covering both `prompt` and `prompt_async` routes - Delete `src/altimate/intent/validation-classifier.ts` and its directory **Conversation logger** - Add `src/session/conversation-logger.ts` — logs the last user+assistant turn (prompt, normalised tool calls, final response, cost/tokens) to `POST /log-conversation` on session idle; collects up to 500 messages to capture multi-step sessions; fire-and-forget, never blocks the UI - Gate logger in `bootstrap.ts` behind `ALTIMATE_LOGGER_DISABLED=true` - Add `docs/docs/configure/logging.md` documenting what is collected and how to opt out - Add Data Collection section to `README.md` with opt-out instructions **Build / cleanup** - Remove duplicate `ALTIMATE_CLI_*` constants from `build.ts` - Remove `load_dotenv` import and dead path-resolution block from `batch_validate.py` - Remove Langfuse reference from `SKILL.md` comment - Update all `uv run` invocations in `SKILL.md` to drop `--with python-dotenv` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 34524f0 commit c3ec5b3

File tree

41 files changed

+2536
-141
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2536
-141
lines changed

.github/workflows/pr-standards.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
owner: context.repo.owner,
3333
repo: context.repo.repo,
3434
path: '.github/TEAM_MEMBERS',
35-
ref: 'dev'
35+
ref: 'main'
3636
});
3737
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
3838
if (members.includes(login)) {
@@ -179,7 +179,7 @@ jobs:
179179
owner: context.repo.owner,
180180
repo: context.repo.repo,
181181
path: '.github/TEAM_MEMBERS',
182-
ref: 'dev'
182+
ref: 'main'
183183
});
184184
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
185185
if (members.includes(login)) {

.gitignore

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,37 @@ target
2323
.scripts
2424
.direnv/
2525

26+
# Python
27+
__pycache__/
28+
*.pyc
29+
*.pyo
30+
*.egg-info/
31+
32+
# SQLite databases (feedback store creates these at runtime)
33+
*.db
34+
35+
# Runtime logs
36+
*.log
37+
logs/
38+
39+
# Large intermediate files at repo root (generated during benchmark runs)
40+
/queries.json
41+
/queries_1k.json
42+
/results/
43+
44+
# Local runtime config
45+
.altimate-code/
46+
47+
# Commit message scratch files
48+
.github/meta/
49+
50+
# Experiment / simulation artifacts
51+
/data/
52+
/experiments/
53+
/models/
54+
/simulation/
55+
2656
# Local dev files
2757
opencode-dev
28-
logs/
2958
*.bun-build
3059
tsconfig.tsbuildinfo

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,20 @@ Full docs at **[altimate-code.sh](https://altimate-code.sh)**.
157157
- [Agent Modes](https://altimate-code.sh/data-engineering/agent-modes/)
158158
- [Configuration](https://altimate-code.sh/configure/model-providers/)
159159
160+
## Data Collection
161+
162+
Altimate Code logs conversation turns (prompt, tool calls, and assistant response) to improve validation quality and agent behavior. Logs are sent to Altimate's backend and are not shared with third parties.
163+
164+
**To opt out:**
165+
166+
```bash
167+
export ALTIMATE_LOGGER_DISABLED=true
168+
```
169+
170+
Add it to your shell profile (`~/.zshrc`, `~/.bashrc`) to make it permanent.
171+
172+
See [`docs/docs/configure/logging.md`](docs/docs/configure/logging.md) for details on what is collected.
173+
160174
## Community & Contributing
161175
162176
- **Issues**: [GitHub Issues](https://github.com/AltimateAI/altimate-code/issues)

bun.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/docs/configure/logging.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Conversation Logging
2+
3+
Altimate Code automatically logs each conversation turn to the Altimate backend. This powers validation, audit, and quality analysis features. Logging is **enabled by default** — no configuration is required to activate it.
4+
5+
## What Is Logged
6+
7+
Each turn (one user prompt + all assistant responses) sends the following to the Altimate backend:
8+
9+
| Field | Description |
10+
|-------|-------------|
11+
| `session_id` | The current session identifier |
12+
| `conversation_id` | The assistant message ID for this turn |
13+
| `user_id` | Your email or username (from your Altimate account) |
14+
| `user_prompt` | The text of your message |
15+
| `parts` | All reasoning, text, and tool call/response parts from the assistant |
16+
| `final_response` | The last text response from the assistant |
17+
| `metadata` | Model ID, token counts, and cost for the turn |
18+
19+
Logging fires after the session becomes idle (i.e., after the assistant finishes responding). Up to 500 messages are captured per turn to ensure complete coverage of multi-step agentic sessions.
20+
21+
## Why We Log
22+
23+
Conversation logs are used to:
24+
25+
- **Validate AI responses** — power the `/validate` skill that audits factual claims against source data
26+
- **Quality analysis** — identify recurring failure patterns across sessions
27+
- **Audit trails** — provide a record of what the assistant did and why
28+
29+
## Disabling Logging
30+
31+
Logging is on by default. To disable it, set the following environment variable before starting Altimate Code:
32+
33+
```bash
34+
export ALTIMATE_LOGGER_DISABLED=true
35+
```
36+
37+
To make this permanent, add it to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.):
38+
39+
```bash
40+
echo 'export ALTIMATE_LOGGER_DISABLED=true' >> ~/.zshrc
41+
source ~/.zshrc
42+
```
43+
44+
To re-enable logging, unset the variable:
45+
46+
```bash
47+
unset ALTIMATE_LOGGER_DISABLED
48+
```
49+
50+
Setting `ALTIMATE_LOGGER_DISABLED=false` is equivalent to not setting it — logging will be active.
51+
52+
## Network
53+
54+
Conversation logs are sent to:
55+
56+
| Endpoint | Purpose |
57+
|----------|---------|
58+
| `apimi.tryaltimate.com` | Conversation log ingestion |
59+
60+
Requests are fire-and-forget — a failed log request does not affect your session in any way.

packages/altimate-code/src/provider/models-snapshot.ts

Lines changed: 2 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
name: altimate-setup
3+
description: Configure Altimate platform credentials for datamate and API access
4+
---
5+
6+
# Altimate Setup
7+
8+
Guide the user through configuring their Altimate platform credentials.
9+
10+
## Steps
11+
12+
1. **Check existing config**: Read `~/.altimate/altimate.json`. If it exists and is valid, show the current config (mask the API key) and ask if they want to update it.
13+
14+
2. **Gather credentials**: Ask the user for:
15+
- **Altimate URL** (default: `https://api.myaltimate.com`)
16+
- **Instance name** (their tenant/org name, e.g. `megatenant`)
17+
- **API key** (from Altimate platform settings)
18+
- **MCP server URL** (optional, default: `https://mcpserver.getaltimate.com/sse`)
19+
20+
3. **Write config**: Create `~/.altimate/` directory if needed, then write `~/.altimate/altimate.json`:
21+
```json
22+
{
23+
"altimateUrl": "<url>",
24+
"altimateInstanceName": "<instance>",
25+
"altimateApiKey": "<key>",
26+
"mcpServerUrl": "<mcp-url>"
27+
}
28+
```
29+
Then set permissions to owner-only: `chmod 600 ~/.altimate/altimate.json`
30+
31+
4. **Validate**: Call the `datamate_manager` tool with `operation: "list"` to verify the credentials work. Report success or failure to the user.

packages/opencode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"scripts": {
99
"typecheck": "tsgo --noEmit",
1010
"test": "bun test --timeout 30000",
11-
"build": "bun run script/build.ts",
11+
"build": "bun run script/build.ts && bun run --conditions=browser ./src/index.ts validate install",
1212
"dev": "bun run --conditions=browser ./src/index.ts",
1313
"db": "bun drizzle-kit"
1414
},

packages/opencode/script/build.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ const migrations = await Promise.all(
7171
)
7272
console.log(`Loaded ${migrations.length} migrations`)
7373

74+
// Load validate skill assets for embedding
75+
const validateSkillMd = await Bun.file(path.join(dir, "src/skill/validate/SKILL.md")).text()
76+
const validateBatchPy = await Bun.file(path.join(dir, "src/skill/validate/batch_validate.py")).text()
77+
console.log("Loaded validate skill assets")
78+
7479
const singleFlag = process.argv.includes("--single")
7580
const baselineFlag = process.argv.includes("--baseline")
7681
const skipInstall = process.argv.includes("--skip-install")
@@ -220,6 +225,8 @@ for (const item of targets) {
220225
OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
221226
OPENCODE_MIGRATIONS: JSON.stringify(migrations),
222227
OPENCODE_CHANGELOG: JSON.stringify(changelog),
228+
ALTIMATE_VALIDATE_SKILL_MD: JSON.stringify(validateSkillMd),
229+
ALTIMATE_VALIDATE_BATCH_PY: JSON.stringify(validateBatchPy),
223230
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
224231
},
225232
})
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import z from "zod"
2+
import path from "path"
3+
import { Global } from "../../global"
4+
import { Filesystem } from "../../util/filesystem"
5+
6+
const DEFAULT_MCP_URL = "https://mcpserver.getaltimate.com/sse"
7+
8+
const AltimateCredentials = z.object({
9+
altimateUrl: z.string(),
10+
altimateInstanceName: z.string(),
11+
altimateApiKey: z.string(),
12+
mcpServerUrl: z.string().optional(),
13+
})
14+
type AltimateCredentials = z.infer<typeof AltimateCredentials>
15+
16+
const DatamateSummary = z.object({
17+
id: z.coerce.string(),
18+
name: z.string(),
19+
description: z.string().nullable().optional(),
20+
integrations: z
21+
.array(
22+
z.object({
23+
id: z.coerce.string(),
24+
tools: z.array(z.object({ key: z.string() })).optional(),
25+
}),
26+
)
27+
.nullable()
28+
.optional(),
29+
memory_enabled: z.boolean().optional(),
30+
privacy: z.string().optional(),
31+
})
32+
33+
const IntegrationSummary = z.object({
34+
id: z.coerce.string(),
35+
name: z.string().optional(),
36+
description: z.string().nullable().optional(),
37+
tools: z
38+
.array(
39+
z.object({
40+
key: z.string(),
41+
name: z.string().optional(),
42+
enable_all: z.array(z.string()).optional(),
43+
}),
44+
)
45+
.optional(),
46+
})
47+
48+
export namespace AltimateApi {
49+
export function credentialsPath(): string {
50+
return path.join(Global.Path.home, ".altimate", "altimate.json")
51+
}
52+
53+
export async function isConfigured(): Promise<boolean> {
54+
return Filesystem.exists(credentialsPath())
55+
}
56+
57+
export async function getCredentials(): Promise<AltimateCredentials> {
58+
const p = credentialsPath()
59+
if (!(await Filesystem.exists(p))) {
60+
throw new Error(`Altimate credentials not found at ${p}`)
61+
}
62+
const raw = JSON.parse(await Filesystem.readText(p))
63+
return AltimateCredentials.parse(raw)
64+
}
65+
66+
async function request(creds: AltimateCredentials, method: string, endpoint: string, body?: unknown) {
67+
const url = `${creds.altimateUrl}${endpoint}`
68+
const res = await fetch(url, {
69+
method,
70+
headers: {
71+
"Content-Type": "application/json",
72+
Authorization: `Bearer ${creds.altimateApiKey}`,
73+
"x-tenant": creds.altimateInstanceName,
74+
},
75+
...(body ? { body: JSON.stringify(body) } : {}),
76+
})
77+
if (!res.ok) {
78+
throw new Error(`API ${method} ${endpoint} failed with status ${res.status}`)
79+
}
80+
return res.json()
81+
}
82+
83+
export async function listDatamates() {
84+
const creds = await getCredentials()
85+
const data = await request(creds, "GET", "/datamates/")
86+
const list = Array.isArray(data) ? data : (data.datamates ?? data.data ?? [])
87+
return list.map((d: unknown) => DatamateSummary.parse(d)) as z.infer<typeof DatamateSummary>[]
88+
}
89+
90+
export async function getDatamate(id: string) {
91+
const creds = await getCredentials()
92+
try {
93+
const data = await request(creds, "GET", `/datamates/${id}/summary`)
94+
const raw = data.datamate ?? data
95+
return DatamateSummary.parse(raw)
96+
} catch (e) {
97+
// Fallback to list if single-item endpoint is unavailable (404)
98+
if (e instanceof Error && e.message.includes("status 404")) {
99+
const all = await listDatamates()
100+
const found = all.find((d) => d.id === id)
101+
if (!found) {
102+
throw new Error(`Datamate with ID ${id} not found`)
103+
}
104+
return found
105+
}
106+
throw e
107+
}
108+
}
109+
110+
export async function createDatamate(payload: {
111+
name: string
112+
description?: string
113+
integrations?: Array<{ id: string; tools: Array<{ key: string }> }>
114+
memory_enabled?: boolean
115+
privacy?: string
116+
}) {
117+
const creds = await getCredentials()
118+
const data = await request(creds, "POST", "/datamates/", payload)
119+
// Backend returns { id: number } for create
120+
const id = String(data.id ?? data.datamate?.id)
121+
return { id, name: payload.name }
122+
}
123+
124+
export async function updateDatamate(
125+
id: string,
126+
payload: {
127+
name?: string
128+
description?: string
129+
integrations?: Array<{ id: string; tools: Array<{ key: string }> }>
130+
memory_enabled?: boolean
131+
privacy?: string
132+
},
133+
) {
134+
const creds = await getCredentials()
135+
const data = await request(creds, "PATCH", `/datamates/${id}`, payload)
136+
const raw = data.datamate ?? data
137+
return DatamateSummary.parse(raw)
138+
}
139+
140+
export async function deleteDatamate(id: string) {
141+
const creds = await getCredentials()
142+
await request(creds, "DELETE", `/datamates/${id}`)
143+
}
144+
145+
export async function listIntegrations() {
146+
const creds = await getCredentials()
147+
const data = await request(creds, "GET", "/datamate_integrations/")
148+
const list = Array.isArray(data) ? data : (data.integrations ?? data.data ?? [])
149+
return list.map((d: unknown) => IntegrationSummary.parse(d)) as z.infer<typeof IntegrationSummary>[]
150+
}
151+
152+
/** Resolve integration IDs to full integration objects with all tools enabled (matching frontend behavior). */
153+
export async function resolveIntegrations(
154+
integrationIds: string[],
155+
): Promise<Array<{ id: string; tools: Array<{ key: string }> }>> {
156+
const allIntegrations = await listIntegrations()
157+
return integrationIds.map((id) => {
158+
const def = allIntegrations.find((i) => i.id === id)
159+
const tools =
160+
def?.tools?.flatMap((t) => (t.enable_all ?? [t.key]).map((k) => ({ key: k }))) ?? []
161+
return { id, tools }
162+
})
163+
}
164+
165+
export function buildMcpConfig(creds: AltimateCredentials, datamateId: string) {
166+
return {
167+
type: "remote" as const,
168+
url: creds.mcpServerUrl ?? DEFAULT_MCP_URL,
169+
oauth: false as const,
170+
headers: {
171+
Authorization: `Bearer ${creds.altimateApiKey}`,
172+
"x-datamate-id": String(datamateId),
173+
"x-tenant": creds.altimateInstanceName,
174+
"x-altimate-url": creds.altimateUrl,
175+
},
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)