Skip to content

Commit 27be244

Browse files
authored
Fix critical trajectory analysis issues (#567)
1 parent 1865c3b commit 27be244

File tree

9 files changed

+255
-45
lines changed

9 files changed

+255
-45
lines changed

edda/edda_mcp/src/providers/io.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,19 @@ pub struct InitiateProjectResult {
3636
pub work_dir: String,
3737
pub template_name: String,
3838
pub template_description: String,
39+
pub file_tree: String,
3940
}
4041

4142
impl ToolResultDisplay for InitiateProjectResult {
4243
fn display(&self) -> String {
4344
format!(
44-
"Successfully copied {} files from {} template to {}\n\nTemplate: {}\n\n{}",
45+
"Successfully copied {} files from {} template to {}\n\nTemplate: {}\n\n{}\n\nFile structure:\n{}",
4546
self.files_copied,
4647
self.template_name,
4748
self.work_dir,
4849
self.template_name,
49-
self.template_description
50+
self.template_description,
51+
self.file_tree
5052
)
5153
}
5254
}
@@ -168,14 +170,77 @@ impl IOProvider {
168170
let template_description = template.description().unwrap_or("".to_string());
169171
let files = template.extract(work_dir)?;
170172

173+
// generate file tree
174+
let file_tree = Self::generate_file_tree(work_dir, &files)?;
175+
171176
Ok(InitiateProjectResult {
172177
files_copied: files.len(),
173178
work_dir: work_dir.display().to_string(),
174179
template_name,
175180
template_description,
181+
file_tree,
176182
})
177183
}
178184

185+
/// Generate a tree-style visualization of the file structure
186+
/// Collapses directories with more than 10 files to avoid clutter
187+
fn generate_file_tree(_base_dir: &Path, files: &[PathBuf]) -> Result<String> {
188+
use std::collections::BTreeMap;
189+
190+
const MAX_FILES_TO_SHOW: usize = 10;
191+
192+
// build a tree structure
193+
let mut tree: BTreeMap<String, Vec<String>> = BTreeMap::new();
194+
195+
for file in files {
196+
let path_str = file.to_string_lossy().to_string();
197+
let parts: Vec<&str> = path_str.split('/').collect();
198+
199+
if parts.len() == 1 {
200+
// root level file
201+
tree.entry("".to_string())
202+
.or_insert_with(Vec::new)
203+
.push(parts[0].to_string());
204+
} else {
205+
// file in subdirectory
206+
let dir = parts[..parts.len() - 1].join("/");
207+
let file_name = parts[parts.len() - 1].to_string();
208+
tree.entry(dir)
209+
.or_insert_with(Vec::new)
210+
.push(file_name);
211+
}
212+
}
213+
214+
// format as tree
215+
let mut output = String::new();
216+
let mut sorted_dirs: Vec<_> = tree.keys().collect();
217+
sorted_dirs.sort();
218+
219+
for dir in sorted_dirs {
220+
let files_in_dir = &tree[dir];
221+
if dir.is_empty() {
222+
// root files - always show all
223+
for file in files_in_dir {
224+
output.push_str(&format!("{}\n", file));
225+
}
226+
} else {
227+
// directory
228+
output.push_str(&format!("{}/\n", dir));
229+
if files_in_dir.len() <= MAX_FILES_TO_SHOW {
230+
// show all files
231+
for file in files_in_dir {
232+
output.push_str(&format!(" {}\n", file));
233+
}
234+
} else {
235+
// collapse large directories
236+
output.push_str(&format!(" ({} files)\n", files_in_dir.len()));
237+
}
238+
}
239+
}
240+
241+
Ok(output)
242+
}
243+
179244
#[tool(
180245
name = "scaffold_data_app",
181246
description = "Initialize a project by copying template files from the default TypeScript (tRPC + React) template to a work directory. Supports force rewrite to wipe and recreate the directory. It sets up a basic project structure, and should be ALWAYS used as the first step in creating a new data or web app."

edda/edda_templates/template_trpc/CLAUDE.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,39 @@ import { strict as assert } from "node:assert";
1414

1515
## Databricks Type Handling:
1616

17+
- **executeQuery REQUIRES Zod schema**: Pass the Zod schema object as second parameter, NOT a TypeScript type annotation
18+
```typescript
19+
// ❌ WRONG - Do NOT use generic type parameter
20+
const result = await client.executeQuery<MyType>(sql);
21+
22+
// ✅ CORRECT - Pass Zod schema as parameter
23+
const mySchema = z.object({ id: z.number(), name: z.string() });
24+
const result = await client.executeQuery(sql, mySchema);
25+
```
1726
- **QueryResult access**: `executeQuery()` returns `{rows: T[], rowCount: number}`. Always use `.rows` property: `const {rows} = await client.executeQuery(...)` or `result.rows.map(...)`
1827
- **Type imports**: Use `import type { T }` (not `import { T }`) when `verbatimModuleSyntax` is enabled
1928
- **Column access**: Use bracket notation `row['column_name']` (TypeScript strict mode requirement)
2029
- **DATE/TIMESTAMP columns**: Databricks returns Date objects. Use `z.coerce.date()` in schemas (never `z.string()` for dates)
2130
- **Dynamic properties**: Cast explicitly `row['order_id'] as number`
2231

32+
### Helper Utilities:
33+
34+
**mapRows<T>(rows, schema)** - Validates and maps raw SQL rows using Zod schema:
35+
```typescript
36+
import { mapRows } from './databricks';
37+
38+
// When you have raw rows and need manual mapping
39+
const rawRows = [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}];
40+
const userSchema = z.object({ id: z.number(), name: z.string() });
41+
const users = mapRows(rawRows, userSchema);
42+
// users is now typed as { id: number; name: string }[]
43+
```
44+
45+
Use this when:
46+
- Processing nested query results
47+
- Manually mapping row data before returning from tRPC
48+
- Need to validate data from non-Databricks sources
49+
2350
## Frontend Styling Guidelines:
2451

2552
### Component Structure Pattern:

edda/edda_templates/template_trpc/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"db:push": "drizzle-kit push --force",
1010
"db:push-ci": "yes | npm run db:push",
1111
"lint": "eslint --cache src/index.ts",
12-
"test": "node --test --import tsx src/*.test.ts"
12+
"test": "sh -c 'ls src/*.test.ts src/*.test.tsx 2>/dev/null | grep -q . || (echo \"Error: No test files found (*.test.ts or *.test.tsx)\" && exit 1); node --test --import tsx src/*.test.ts'"
1313
},
1414
"dependencies": {
1515
"@databricks/sql": "^1.12.0",

edda/edda_templates/template_trpc/server/src/databricks.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@
55
// const myTableSchema = z.object({
66
// id: z.number(),
77
// name: z.string(),
8-
// created_at: z.string(),
8+
// created_at: z.coerce.date(),
99
// });
1010
//
1111
// const client = new DatabricksClient();
12+
//
13+
// // ✅ CORRECT - Pass Zod schema (not TypeScript type)
1214
// const result = await client.executeQuery("SELECT * FROM my_table", myTableSchema);
13-
// // result.rows is now validated and typed as MyTable[]
15+
// // result.rows is now validated and typed as z.infer<typeof myTableSchema>[]
16+
//
17+
// // ❌ WRONG - Do NOT use generic type parameter alone
18+
// // const result = await client.executeQuery<MyType>("SELECT ...");
19+
// // This will cause runtime errors!
1420

1521
import { DBSQLClient } from "@databricks/sql";
1622
import type { ConnectionOptions } from "@databricks/sql/dist/contracts/IDBSQLClient";
@@ -75,9 +81,21 @@ export class DatabricksClient {
7581
}
7682
}
7783

78-
async executeQuery<T extends z.ZodTypeAny = typeof defaultRowSchema>(
84+
/**
85+
* Execute a SQL query against Databricks and validate results with Zod schema.
86+
*
87+
* @param sql - SQL query string
88+
* @param schema - Zod schema for row validation (REQUIRED - pass the schema, not a TypeScript type)
89+
* @returns QueryResult with validated and typed rows
90+
*
91+
* @example
92+
* const schema = z.object({ id: z.number(), name: z.string() });
93+
* const result = await client.executeQuery("SELECT id, name FROM users", schema);
94+
* // result.rows is typed as { id: number; name: string }[]
95+
*/
96+
async executeQuery<T extends z.ZodTypeAny>(
7997
sql: string,
80-
schema?: T,
98+
schema: T,
8199
): Promise<QueryResult<z.infer<T>>> {
82100
try {
83101
const client = new DBSQLClient();
@@ -92,8 +110,8 @@ export class DatabricksClient {
92110
await session.close();
93111
await connection.close();
94112

95-
// Apply schema validation if provided
96-
const rows = schema ? result.map((row) => schema.parse(row)) : result;
113+
// Apply schema validation
114+
const rows = result.map((row) => schema.parse(row));
97115
return { rows: rows as z.infer<T>[], rowCount: rows.length };
98116
} catch (error) {
99117
console.error("Databricks SQL query error:", error);
@@ -106,3 +124,21 @@ export class DatabricksClient {
106124
}
107125
}
108126
}
127+
128+
/**
129+
* Helper utility to map and validate raw SQL rows using a Zod schema.
130+
* Useful when you have raw rows from nested queries or need manual mapping.
131+
*
132+
* @param rows - Array of raw SQL rows (Record<string, SqlValue>)
133+
* @param schema - Zod schema for validation
134+
* @returns Array of validated and typed objects
135+
*
136+
* @example
137+
* const rawRows = [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}];
138+
* const schema = z.object({ id: z.number(), name: z.string() });
139+
* const users = mapRows(rawRows, schema);
140+
* // users is typed as { id: number; name: string }[]
141+
*/
142+
export function mapRows<T>(rows: SqlRow[], schema: z.ZodSchema<T>): T[] {
143+
return rows.map((row) => schema.parse(row));
144+
}

edda/edda_templates/template_trpc/server/src/server.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { Server } from "node:http";
66
process.env["DATABRICKS_HOST"] =
77
process.env["DATABRICKS_HOST"] || "https://dummy.databricks.com";
88
process.env["DATABRICKS_TOKEN"] = process.env["DATABRICKS_TOKEN"] || "dummy_token";
9+
process.env["DATABRICKS_WAREHOUSE_ID"] =
10+
process.env["DATABRICKS_WAREHOUSE_ID"] || "dummy_warehouse_id";
911

1012
test("server starts and responds to healthcheck", async () => {
1113
// dynamic import to ensure env vars are set first
@@ -34,3 +36,37 @@ test("server starts and responds to healthcheck", async () => {
3436
}
3537
}
3638
});
39+
40+
// Example: Testing tRPC procedures directly without HTTP server
41+
// This is faster and simpler for most tests
42+
//
43+
// test("getUsers returns array of users", async () => {
44+
// const { appRouter } = await import("./index");
45+
// const { initTRPC } = await import("@trpc/server");
46+
//
47+
// // create tRPC caller - no HTTP server needed
48+
// const t = initTRPC.create();
49+
// const caller = t.createCallerFactory(appRouter)({});
50+
//
51+
// const result = await caller.getUsers();
52+
//
53+
// // validate structure
54+
// assert.ok(Array.isArray(result));
55+
// if (result.length > 0) {
56+
// assert.ok(result[0].id);
57+
// assert.ok(result[0].name);
58+
// }
59+
// });
60+
//
61+
// test("getMetrics with input parameter", async () => {
62+
// const { appRouter } = await import("./index");
63+
// const { initTRPC } = await import("@trpc/server");
64+
//
65+
// const t = initTRPC.create();
66+
// const caller = t.createCallerFactory(appRouter)({});
67+
//
68+
// const result = await caller.getMetrics({ category: "sales" });
69+
//
70+
// assert.ok(Array.isArray(result));
71+
// // add assertions for your expected data structure
72+
// });

klaudbiusz/cli/analyze_trajectories.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,20 @@ def load_trajectory(path: Path) -> list[TrajectoryStep]:
3838
return steps
3939

4040

41-
def format_tool_arguments(args: dict) -> str:
42-
"""Format tool arguments as readable JSON."""
43-
return json.dumps(args, indent=2)
41+
def format_tool_arguments(args: dict, max_length: int = 8192) -> str:
42+
"""Format tool arguments as readable JSON, truncating long strings."""
43+
44+
def truncate_value(value):
45+
if isinstance(value, str) and len(value) > max_length:
46+
return f"[truncated {len(value)} chars]"
47+
elif isinstance(value, dict):
48+
return {k: truncate_value(v) for k, v in value.items()}
49+
elif isinstance(value, list):
50+
return [truncate_value(item) for item in value]
51+
return value
52+
53+
truncated_args = truncate_value(args)
54+
return json.dumps(truncated_args, indent=2)
4455

4556

4657
def format_trajectory_to_markdown(steps: list[TrajectoryStep]) -> str:
@@ -73,12 +84,16 @@ def format_trajectory_to_markdown(steps: list[TrajectoryStep]) -> str:
7384
if result.get("is_error"):
7485
lines.append("**⚠️ ERROR**")
7586
lines.append("```")
76-
lines.append(result.get("content", ""))
87+
content = result.get("content", "")
88+
# truncate long tool results (e.g. base64 screenshots)
89+
if isinstance(content, str) and len(content) > 8192:
90+
lines.append(f"[truncated {len(content)} chars]")
91+
else:
92+
lines.append(content)
7793
lines.append("```")
7894
lines.append("")
7995

8096
lines.append("---\n")
81-
8297
return "\n".join(lines)
8398

8499

@@ -101,7 +116,6 @@ async def analyze_single_trajectory(trajectory_md: str, app_name: str, model: st
101116
Provide a concise analysis focusing on actionable insights."""
102117

103118
logger.info(f"🔍 Analyzing trajectory: {app_name}")
104-
105119
response = await litellm.acompletion(
106120
model=model,
107121
messages=[{"role": "user", "content": prompt}],
@@ -165,7 +179,6 @@ async def analyze_trajectories_async(
165179
trajectory_data = [
166180
(path.parent.name, format_trajectory_to_markdown(load_trajectory(path))) for path in trajectory_paths
167181
]
168-
169182
tasks = [
170183
analyze_single_trajectory(trajectory_md, app_name, map_model) for app_name, trajectory_md in trajectory_data
171184
]

klaudbiusz/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ venv = ".venv"
3535

3636
[dependency-groups]
3737
dev = [
38+
"pdbpp>=0.11.7",
3839
"pyright>=1.1.406",
3940
"ruff>=0.14.3",
4041
]

0 commit comments

Comments
 (0)