mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
refactor example import
This commit is contained in:
parent
ea4159a94b
commit
97e47faca8
7 changed files with 46 additions and 78 deletions
|
|
@ -10,8 +10,7 @@
|
|||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"examples"
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"rowboatx": "bin/app.js"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { stdin as input, stdout as output } from "node:process";
|
|||
import fs from "fs";
|
||||
import { promises as fsp } from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { WorkDir, getModelConfig, updateModelConfig } from "./application/config/config.js";
|
||||
import { RunEvent } from "./application/entities/run-events.js";
|
||||
import { createInterface, Interface } from "node:readline/promises";
|
||||
|
|
@ -12,12 +11,8 @@ import { ToolCallPart } from "./application/entities/message.js";
|
|||
import { Agent } from "./application/entities/agent.js";
|
||||
import { McpServerConfig, McpServerDefinition } from "./application/entities/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { Flavor, ModelConfig } from "./application/entities/models.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const PackageRoot = path.resolve(__dirname, "..");
|
||||
const ExamplesDir = path.join(PackageRoot, "examples");
|
||||
import { Flavor } from "./application/entities/models.js";
|
||||
import { examples } from "./examples/index.js";
|
||||
|
||||
export async function updateState(agent: string, runId: string) {
|
||||
const state = new AgentState(agent, runId);
|
||||
|
|
@ -413,51 +408,14 @@ function renderCurrentModel(provider: string, flavor: string, model: string) {
|
|||
console.log("");
|
||||
}
|
||||
|
||||
const ExampleSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
"post-install-instructions": z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
entryAgent: z.string().optional(),
|
||||
agents: z.array(Agent).min(1),
|
||||
mcpServers: z.record(z.string(), McpServerDefinition).optional(),
|
||||
}).refine(
|
||||
(data) => !data.entryAgent || data.agents.some((agent) => agent.name === data.entryAgent),
|
||||
{
|
||||
message: "entryAgent must reference one of the defined agents",
|
||||
path: ["entryAgent"],
|
||||
},
|
||||
);
|
||||
|
||||
async function readExampleFile(exampleName: string): Promise<string> {
|
||||
const examplePath = path.join(ExamplesDir, `${exampleName}.json`);
|
||||
try {
|
||||
return await fsp.readFile(examplePath, "utf8");
|
||||
} catch (error: any) {
|
||||
if (error?.code === "ENOENT") {
|
||||
const availableExamples = await listAvailableExamples();
|
||||
const listMessage = availableExamples.length
|
||||
? `Available examples: ${availableExamples.join(", ")}`
|
||||
: "No packaged examples were found.";
|
||||
throw new Error(`Unknown example '${exampleName}'. ${listMessage}`);
|
||||
}
|
||||
// Re-throw other errors (permission issues, etc.)
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function listAvailableExamples(): Promise<string[]> {
|
||||
try {
|
||||
const entries = await fsp.readdir(ExamplesDir);
|
||||
return entries
|
||||
.filter((entry) => entry.endsWith(".json"))
|
||||
.map((entry) => entry.replace(/\.json$/, ""))
|
||||
.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(examples);
|
||||
}
|
||||
|
||||
async function writeAgents(agents: z.infer<typeof Agent>[]) {
|
||||
async function writeAgents(agents: z.infer<typeof Agent>[] | undefined) {
|
||||
if (!agents) {
|
||||
return;
|
||||
}
|
||||
await fsp.mkdir(path.join(WorkDir, "agents"), { recursive: true });
|
||||
await Promise.all(
|
||||
agents.map(async (agent) => {
|
||||
|
|
@ -509,22 +467,17 @@ async function mergeMcpServers(servers: Record<string, z.infer<typeof McpServerD
|
|||
}
|
||||
|
||||
export async function importExample(exampleName: string) {
|
||||
const raw = await readExampleFile(exampleName);
|
||||
const parsed = ExampleSchema.parse(JSON.parse(raw));
|
||||
const entryAgentName = parsed.entryAgent ?? parsed.agents[0]?.name;
|
||||
if (!entryAgentName) {
|
||||
throw new Error(`Example '${exampleName}' does not define any agents to run.`);
|
||||
}
|
||||
const postInstallInstructions = parsed["post-install-instructions"];
|
||||
await writeAgents(parsed.agents);
|
||||
const example = examples[exampleName];
|
||||
const postInstallInstructions = example.instructions;
|
||||
await writeAgents(example.agents);
|
||||
let serverMerge = { added: [] as string[], skipped: [] as string[] };
|
||||
if (parsed.mcpServers) {
|
||||
serverMerge = await mergeMcpServers(parsed.mcpServers);
|
||||
if (example.mcpServers) {
|
||||
serverMerge = await mergeMcpServers(example.mcpServers);
|
||||
}
|
||||
return {
|
||||
id: parsed.id,
|
||||
entryAgent: entryAgentName,
|
||||
importedAgents: parsed.agents.map((agent) => agent.name),
|
||||
id: example.id,
|
||||
entryAgent: example.entryAgent,
|
||||
importedAgents: example.agents?.map((agent) => agent.name) ?? [],
|
||||
addedServers: serverMerge.added,
|
||||
skippedServers: serverMerge.skipped,
|
||||
postInstallInstructions,
|
||||
|
|
|
|||
|
|
@ -12,19 +12,6 @@ let modelConfig: z.infer<typeof ModelConfig> | null = null;
|
|||
|
||||
const baseMcpConfig: z.infer<typeof McpServerConfig> = {
|
||||
mcpServers: {
|
||||
firecrawl: {
|
||||
command: "npx",
|
||||
args: ["-y", "supergateway", "--stdio", "npx -y firecrawl-mcp"],
|
||||
env: {
|
||||
FIRECRAWL_API_KEY: "fc-aaacee4bdd164100a4d83af85bef6fdc",
|
||||
},
|
||||
},
|
||||
test: {
|
||||
url: "http://localhost:3000",
|
||||
headers: {
|
||||
"Authorization": "Bearer test",
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
12
apps/cli/src/application/entities/example.ts
Normal file
12
apps/cli/src/application/entities/example.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import z from "zod"
|
||||
import { Agent } from "./agent.js"
|
||||
import { McpServerDefinition } from "./mcp.js"
|
||||
|
||||
export const Example = z.object({
|
||||
id: z.string(),
|
||||
instructions: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
entryAgent: z.string().optional(),
|
||||
agents: z.array(Agent).optional(),
|
||||
mcpServers: z.record(z.string(), McpServerDefinition).optional(),
|
||||
});
|
||||
8
apps/cli/src/examples/gemini3-test.json
Normal file
8
apps/cli/src/examples/gemini3-test.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "gemini3_svg_pelican",
|
||||
"provider": "google",
|
||||
"model": "gemini-3.0-pro",
|
||||
"description": "Outputs a single valid SVG depicting a pelican riding a bicycle.",
|
||||
"instructions": "You must output only a single, valid, self-contained SVG XML depicting a pelican riding a bicycle. Requirements: 1) Output must be ONLY raw SVG XML (no markdown fences, no explanations). 2) Use viewBox=\"0 0 512 512\" and set width/height to 512. 3) Include clear, recognizable pelican and bicycle using basic shapes/paths. 4) No external refs, images, scripts, or styles; use inline attributes only. 5) Keep IDs minimal; keep total file size reasonable.",
|
||||
"tools": {}
|
||||
}
|
||||
9
apps/cli/src/examples/index.ts
Normal file
9
apps/cli/src/examples/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import twitterPodcast from './twitter-podcast.json' with { type: 'json' };
|
||||
import gemini3Test from './gemini3-test.json' with { type: 'json' };
|
||||
import { Example } from '../application/entities/example.js';
|
||||
import z from 'zod';
|
||||
|
||||
export const examples: Record<string, z.infer<typeof Example>> = {
|
||||
"twitter-podcast": Example.parse(twitterPodcast),
|
||||
"gemini3-test": Example.parse(gemini3Test),
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"id": "twitter-podcast",
|
||||
"post-import-instructions": "This example workflow generates a narrated podcast episode from recent AI-related tweets using multiple agents.",
|
||||
"instructions": "This example workflow generates a narrated podcast episode from recent AI-related tweets using multiple agents.",
|
||||
"description": "Generates a narrated podcast episode from recent AI-related tweets using multiple agents.",
|
||||
"entryAgent": "tweet-podcast",
|
||||
"agents": [
|
||||
Loading…
Add table
Add a link
Reference in a new issue