mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-03 20:41:07 +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": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"bin",
|
"bin"
|
||||||
"examples"
|
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"rowboatx": "bin/app.js"
|
"rowboatx": "bin/app.js"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { stdin as input, stdout as output } from "node:process";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { promises as fsp } from "fs";
|
import { promises as fsp } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import { WorkDir, getModelConfig, updateModelConfig } from "./application/config/config.js";
|
import { WorkDir, getModelConfig, updateModelConfig } from "./application/config/config.js";
|
||||||
import { RunEvent } from "./application/entities/run-events.js";
|
import { RunEvent } from "./application/entities/run-events.js";
|
||||||
import { createInterface, Interface } from "node:readline/promises";
|
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 { Agent } from "./application/entities/agent.js";
|
||||||
import { McpServerConfig, McpServerDefinition } from "./application/entities/mcp.js";
|
import { McpServerConfig, McpServerDefinition } from "./application/entities/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Flavor, ModelConfig } from "./application/entities/models.js";
|
import { Flavor } from "./application/entities/models.js";
|
||||||
|
import { examples } from "./examples/index.js";
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const PackageRoot = path.resolve(__dirname, "..");
|
|
||||||
const ExamplesDir = path.join(PackageRoot, "examples");
|
|
||||||
|
|
||||||
export async function updateState(agent: string, runId: string) {
|
export async function updateState(agent: string, runId: string) {
|
||||||
const state = new AgentState(agent, runId);
|
const state = new AgentState(agent, runId);
|
||||||
|
|
@ -413,51 +408,14 @@ function renderCurrentModel(provider: string, flavor: string, model: string) {
|
||||||
console.log("");
|
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[]> {
|
async function listAvailableExamples(): Promise<string[]> {
|
||||||
try {
|
return Object.keys(examples);
|
||||||
const entries = await fsp.readdir(ExamplesDir);
|
|
||||||
return entries
|
|
||||||
.filter((entry) => entry.endsWith(".json"))
|
|
||||||
.map((entry) => entry.replace(/\.json$/, ""))
|
|
||||||
.sort();
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 fsp.mkdir(path.join(WorkDir, "agents"), { recursive: true });
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
agents.map(async (agent) => {
|
agents.map(async (agent) => {
|
||||||
|
|
@ -509,22 +467,17 @@ async function mergeMcpServers(servers: Record<string, z.infer<typeof McpServerD
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importExample(exampleName: string) {
|
export async function importExample(exampleName: string) {
|
||||||
const raw = await readExampleFile(exampleName);
|
const example = examples[exampleName];
|
||||||
const parsed = ExampleSchema.parse(JSON.parse(raw));
|
const postInstallInstructions = example.instructions;
|
||||||
const entryAgentName = parsed.entryAgent ?? parsed.agents[0]?.name;
|
await writeAgents(example.agents);
|
||||||
if (!entryAgentName) {
|
|
||||||
throw new Error(`Example '${exampleName}' does not define any agents to run.`);
|
|
||||||
}
|
|
||||||
const postInstallInstructions = parsed["post-install-instructions"];
|
|
||||||
await writeAgents(parsed.agents);
|
|
||||||
let serverMerge = { added: [] as string[], skipped: [] as string[] };
|
let serverMerge = { added: [] as string[], skipped: [] as string[] };
|
||||||
if (parsed.mcpServers) {
|
if (example.mcpServers) {
|
||||||
serverMerge = await mergeMcpServers(parsed.mcpServers);
|
serverMerge = await mergeMcpServers(example.mcpServers);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: parsed.id,
|
id: example.id,
|
||||||
entryAgent: entryAgentName,
|
entryAgent: example.entryAgent,
|
||||||
importedAgents: parsed.agents.map((agent) => agent.name),
|
importedAgents: example.agents?.map((agent) => agent.name) ?? [],
|
||||||
addedServers: serverMerge.added,
|
addedServers: serverMerge.added,
|
||||||
skippedServers: serverMerge.skipped,
|
skippedServers: serverMerge.skipped,
|
||||||
postInstallInstructions,
|
postInstallInstructions,
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,6 @@ let modelConfig: z.infer<typeof ModelConfig> | null = null;
|
||||||
|
|
||||||
const baseMcpConfig: z.infer<typeof McpServerConfig> = {
|
const baseMcpConfig: z.infer<typeof McpServerConfig> = {
|
||||||
mcpServers: {
|
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",
|
"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.",
|
"description": "Generates a narrated podcast episode from recent AI-related tweets using multiple agents.",
|
||||||
"entryAgent": "tweet-podcast",
|
"entryAgent": "tweet-podcast",
|
||||||
"agents": [
|
"agents": [
|
||||||
Loading…
Add table
Add a link
Reference in a new issue