History

Enhance Tool Explorer and OpenAPI Handling

- Update styling logic in the ToolRow and GroupNode components to differentiate selected and source tools, improving visual clarity. - Modify OpenAPI preparation to always include parameter schemas, ensuring consistent schema handling. - Add tests to validate input and output hints for OpenAPI tools, ensuring accurate type representation. - Update OpenAPI cache versioning to reflect recent changes and improvements in tool discovery. - Refactor workspace tool inventory logic to streamline state management and improve performance.

Rhys Sullivan committed 8 months ago

Commit e8d1e821 parent 8ae75e18 files · +97 −11Browse files

Unified · Split

8 files
  1. executor/apps/web/src/components/tools/explorer-groups.tsx +3 −1
  2. executor/apps/web/src/components/tools/explorer-rows.tsx +4 −2
  3. executor/packages/core/src/openapi-prepare.ts +1 −1
  4. executor/packages/core/src/openapi-typing.test.ts +2 −0
  5. executor/packages/core/src/openapi/real-specs.test.ts +30 −0
  6. executor/packages/core/src/tool-discovery.test.ts +55 −0
  7. executor/packages/database/src/runtime/tool_source_loading.ts +1 −1
  8. executor/packages/database/src/runtime/workspace_tools.ts +1 −6
executor/apps/web/src/components/tools/explorer-groups.tsx+3−1
@@ -80,7 +80,9 @@ export function GroupNode({ · 79 unmodified lines
80 "sticky bg-background/95 backdrop-blur-sm",80 "sticky bg-background/95 backdrop-blur-sm",
81 isExpanded && "border-b border-border/30",81 isExpanded && "border-b border-border/30",
82 isGroupSelected82 isGroupSelected
83 ? "bg-primary/10 ring-1 ring-primary/20"83 ? isSource
84 ? "bg-primary/10 ring-1 ring-primary/20"
85 : "bg-accent/20 ring-1 ring-accent/30"
84 : "hover:bg-accent/30",86 : "hover:bg-accent/30",
85 )}87 )}
86 style={{88 style={{
executor/apps/web/src/components/tools/explorer-rows.tsx+4−2
@@ -50,9 +50,11 @@ const ToolRow = memo(function ToolRow({ · 49 unmodified lines
50 className={cn(50 className={cn(
51 "flex items-center gap-2 px-2 py-1.5 transition-colors cursor-pointer group/tool",51 "flex items-center gap-2 px-2 py-1.5 transition-colors cursor-pointer group/tool",
52 expanded52 expanded
53 ? "sticky bg-background/95 backdrop-blur-sm bg-accent/30"53 ? selected
54 ? "sticky bg-accent ring-1 ring-accent/30"
55 : "sticky bg-card"
54 : selected56 : selected
55 ? "bg-primary/5 ring-1 ring-primary/10"57 ? "bg-accent ring-1 ring-accent/30"
56 : "hover:bg-accent/20",58 : "hover:bg-accent/20",
57 )}59 )}
58 style={{60 style={{
executor/packages/core/src/openapi-prepare.ts+1−1
@@ -233,7 +233,7 @@ export async function prepareOpenApiSpec( · 232 unmodified lines
233 {233 {
234 includeSchemas: profile === "full",234 includeSchemas: profile === "full",
235 includeTypeHints: true,235 includeTypeHints: true,
236 includeParameterSchemas: profile === "full",236 includeParameterSchemas: true,
237 resolveSchemaRefs: profile === "full",237 resolveSchemaRefs: profile === "full",
238 },238 },
239 );239 );
executor/packages/core/src/openapi-typing.test.ts+2−0
@@ -100,6 +100,8 @@ describe("OpenAPI schema-first typing", () => { · 99 unmodified lines
100 expect(getTool!.typing?.outputSchema).toBeDefined();100 expect(getTool!.typing?.outputSchema).toBeDefined();
101 expect(getTool!.typing?.requiredInputKeys ?? []).toContain("id");101 expect(getTool!.typing?.requiredInputKeys ?? []).toContain("id");
102 expect(getTool!.typing?.previewInputKeys ?? []).toContain("include");102 expect(getTool!.typing?.previewInputKeys ?? []).toContain("include");
103 expect(getTool!.typing?.inputHint).toContain("id: string");
104 expect(getTool!.typing?.inputHint).toContain("include?:");
103 expect(getTool!.typing?.typedRef).toBeDefined();105 expect(getTool!.typing?.typedRef).toBeDefined();
104 });106 });
105 107
executor/packages/core/src/openapi/real-specs.test.ts+30−0
@@ -301,4 +301,34 @@ describe("real-world OpenAPI specs", () => { · 300 unmodified lines
301 },301 },
302 300_000,302 300_000,
303 );303 );
304
305 test(
306 "openai: assistants cancel run keeps path parameter types in inventory mode",
307 async () => {
308 const openAiUrl = "https://app.stainless.com/api/spec/documented/openai/openapi.documented.yml";
309 const prepared = await prepareOpenApiSpec(openAiUrl, "openai", {
310 includeDts: false,
311 profile: "inventory",
312 });
313
314 const tools = buildOpenApiToolsFromPrepared(
315 {
316 type: "openapi",
317 name: "openai",
318 spec: openAiUrl,
319 baseUrl: prepared.servers[0] || "https://api.openai.com",
320 },
321 prepared,
322 );
323
324 const tool = tools.find((t) => t.path === "openai.assistants.cancel_run");
325 expect(tool).toBeDefined();
326
327 const inputHint = tool!.typing?.inputHint ?? "";
328 expect(inputHint).toContain("thread_id: string");
329 expect(inputHint).toContain("run_id: string");
330 expect(inputHint.includes("unknown")).toBe(false);
331 },
332 300_000,
333 );
304});334});
executor/packages/core/src/tool-discovery.test.ts+55−0
@@ -412,6 +412,61 @@ test("discover depth 0 keeps full signatures", async () => { · 411 unmodified lines
412 );412 );
413});413});
414 414
415test("discover prefers schema over lossy typed hints", async () => {
416 const tool = createDiscoverTool([
417 {
418 path: "openai.batch.create_batch",
419 description: "Create batch",
420 approval: "required",
421 source: "openapi:openai",
422 typing: {
423 inputHint: "{ input_file_id: ...; endpoint: ... }",
424 outputHint: "{ id: string; errors?: { message?:... } }",
425 inputSchema: {
426 type: "object",
427 properties: {
428 input_file_id: { type: "string" },
429 endpoint: { type: "string" },
430 completion_window: { type: "string" },
431 },
432 required: ["input_file_id", "endpoint", "completion_window"],
433 },
434 outputSchema: {
435 type: "object",
436 properties: {
437 id: { type: "string" },
438 errors: {
439 type: "object",
440 properties: {
441 message: { type: "string" },
442 },
443 },
444 },
445 required: ["id"],
446 },
447 },
448 run: async () => ({ id: "b_123" }),
449 } satisfies ToolDefinition,
450 ]);
451
452 const result = await tool.run(
453 { query: "create batch", depth: 2 },
454 { taskId: "t", workspaceId: TEST_WORKSPACE_ID, isToolAllowed: () => true },
455 ) as {
456 results: Array<{
457 signatureInfo: {
458 input: string;
459 output: string;
460 };
461 }>;
462 };
463
464 expect(result.results[0]?.signatureInfo.input).toContain("input_file_id: string");
465 expect(result.results[0]?.signatureInfo.input.includes("...")).toBe(false);
466 expect(result.results[0]?.signatureInfo.output).toContain("message?: string");
467 expect(result.results[0]?.signatureInfo.output.includes("...")).toBe(false);
468});
469
415test("discover returns null bestPath when there are no matches", async () => {470test("discover returns null bestPath when there are no matches", async () => {
416 const tool = createDiscoverTool([471 const tool = createDiscoverTool([
417 {472 {
executor/packages/database/src/runtime/tool_source_loading.ts+1−1
@@ -30,7 +30,7 @@ import { · 29 unmodified lines
30 30
31const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000;31const OPENAPI_SPEC_CACHE_TTL_MS = 5 * 60 * 60_000;
32 32
33const OPENAPI_PREPARED_CACHE_VERSION = "openapi_v5";33const OPENAPI_PREPARED_CACHE_VERSION = "openapi_v6";
34 34
35const openApiAuthModeSchema = z.enum(["static", "account", "workspace", "organization"]);35const openApiAuthModeSchema = z.enum(["static", "account", "workspace", "organization"]);
36 36
executor/packages/database/src/runtime/workspace_tools.ts+1−6
@@ -1504,11 +1504,6 @@ export async function rebuildWorkspaceToolInventoryForContext( · 1503 unmodified lines
1504 sources.filter((source) => source.enabled),1504 sources.filter((source) => source.enabled),
1505 );1505 );
1506 1506
1507 const alreadyReady = Boolean(
1508 state?.readyBuildId
1509 && !state?.buildingBuildId
1510 && state?.signature === registrySignature,
1511 );
1512 const alreadyBuildingTarget = Boolean(1507 const alreadyBuildingTarget = Boolean(
1513 state?.buildingBuildId1508 state?.buildingBuildId
1514 && state?.buildingSignature === registrySignature,1509 && state?.buildingSignature === registrySignature,
@@ -1523,7 +1518,7 @@ export async function rebuildWorkspaceToolInventoryForContext( · 8 unmodified lines
1523 && Date.now() - buildingStartedAt > REGISTRY_BUILD_STALE_MS,1518 && Date.now() - buildingStartedAt > REGISTRY_BUILD_STALE_MS,
1524 );1519 );
1525 1520
1526 if (alreadyReady || (alreadyBuildingTarget && !staleBuildingTarget)) {1521 if (alreadyBuildingTarget && !staleBuildingTarget) {
1527 return { rebuilt: false };1522 return { rebuilt: false };
1528 }1523 }
1529 1524