Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions packages/_example/src/forest/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { Schema } from './typings';
import type { AgentOptions } from '@forestadmin/agent';

import { createAgent } from '@forestadmin/agent';
import { createAiProvider } from '@forestadmin/ai-proxy';
import { createMongoDataSource } from '@forestadmin/datasource-mongo';
import { createMongooseDataSource } from '@forestadmin/datasource-mongoose';
import { createSequelizeDataSource } from '@forestadmin/datasource-sequelize';
Expand Down Expand Up @@ -96,13 +95,5 @@ export default function makeAgent() {
.customizeCollection('post', customizePost)
.customizeCollection('comment', customizeComment)
.customizeCollection('review', customizeReview)
.customizeCollection('sales', customizeSales)
.addAi(
createAiProvider({
model: 'gpt-4o',
provider: 'openai',
name: 'test',
apiKey: process.env.OPENAI_API_KEY,
}),
);
.customizeCollection('sales', customizeSales);
}
7 changes: 2 additions & 5 deletions packages/agent-toolkit/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Overview

`@forestadmin/agent-toolkit` is the lowest-level shared package in the monorepo. It holds two cross-cutting contracts that sibling packages depend on: the `BusinessError` hierarchy and the AI-proxy provider interfaces. It deliberately has **zero `@forestadmin/*` dependencies** so that any package can depend on it without creating a cycle.
`@forestadmin/agent-toolkit` is the lowest-level shared package in the monorepo. It holds the cross-cutting `BusinessError` hierarchy that sibling packages depend on. It deliberately has **zero `@forestadmin/*` dependencies** so that any package can depend on it without creating a cycle.

## Architecture

Expand All @@ -13,10 +13,7 @@ The package is tiny but load-bearing — its value is in being the single source
- **`src/errors.ts` — `BusinessError` hierarchy.** `BusinessError` plus the HTTP-flavored subclasses (`ValidationError`, `BadRequestError`, `UnprocessableError`, `ForbiddenError`, `NotFoundError`, `UnauthorizedError`, `TooManyRequestsError`, `InternalServerError`). The agent's error middleware maps these to HTTP statuses. `datasource-toolkit/src/errors.ts` **re-exports `BusinessError`/`ValidationError` from here** for backward compatibility and builds its own domain errors on top, so this is the canonical definition — changing it ripples outward.
- Subclasses set `baseBusinessErrorName` so error type can be detected across mismatched package versions. **Use `BusinessError.isOfType(err, SomeError)` rather than `instanceof`** — `instanceof` is unreliable when two packages resolve to different copies of this module.

- **`src/interfaces/ai.ts` — AI provider contract.** Type-only interfaces (`AiProviderDefinition`, `AiProviderMeta`, `AiRouter`) that define how an AI provider plugs into the agent. `@forestadmin/ai-proxy` *implements* these (via `createAiProvider`); `@forestadmin/agent` *consumes* them (in its `ai-proxy` route and schema generator). `AiRouter.route()` implementations should throw the `BusinessError` subclasses above for correct HTTP mapping.
- `Logger`/`LoggerLevel` here are **inlined copies** of datasource-toolkit's types (to avoid the dependency). They must stay structurally compatible — if datasource-toolkit's `Logger` changes, update these by hand.

- **`src/index.ts`** re-exports everything; AI symbols are exported as `type` (no runtime code).
- **`src/index.ts`** re-exports the error hierarchy.

## Commands

Expand Down
1 change: 0 additions & 1 deletion packages/agent-toolkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export * from './errors';
export type { AiProviderDefinition, AiProviderMeta, AiRouter } from './interfaces/ai';
35 changes: 0 additions & 35 deletions packages/agent-toolkit/src/interfaces/ai.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/agent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The `Agent` class (`src/agent.ts`, default export, aliased as `AgentBuilder` in
- **Routes** (`src/routes/`, assembled by `makeRoutes` in `routes/index.ts`) are generated dynamically per datasource: root routes, then CRUD/capabilities/native-query per collection, api-charts, related routes (built by relation type — `ManyToMany`/`OneToMany` get list/count/csv/associate/dissociate, `OneToOne`/`ManyToOne` get update-relation), action routes, plus opt-in AI and workflow-executor routes. There is no per-segment route. Each route extends `BaseRoute`/`CollectionRoute` and registers itself onto a `@koa/router`. Routes are sorted by `RouteType` (`src/types.ts`) so logger/error/auth middleware load before private routes — order is load-bearing, do not reorder by alphabet.
- **Services** (`src/services/`, built by `makeServices`) are the shared dependencies injected into every route: `authorization` (delegates permission checks to `@forestadmin/forestadmin-client`), `serializer` (JSON:API), `chartHandler`, and `segmentQueryHandler` (applies the requested segment as a filter inside the `list`/`count` routes via `handleLiveQuerySegmentFilter` — this is where segments are handled, not at route generation).
- **`FrameworkMounter`** (`src/framework-mounter.ts`, the `Agent` base class) handles mounting onto Express/Fastify/Koa/NestJS/standalone. It mounts forest routes at `/{prefix}/forest` and remounts the router in place on every `restart()` (triggered by `onRefreshCustomizations`) without restarting the HTTP server.
- **MCP and AI** are opt-in. `mountAiMcpServer()` defers loading `@forestadmin/mcp-server` via dynamic `import()` so non-MCP users don't pay for it; `addAi()` wires an `@forestadmin/ai-proxy` provider. The MCP HTTP callback is injected ahead of the body parser via `McpMiddleware`.
- **MCP** is opt-in. `mountAiMcpServer()` defers loading `@forestadmin/mcp-server` via dynamic `import()` so non-MCP users don't pay for it. The MCP HTTP callback is injected ahead of the body parser via `McpMiddleware`.

`SchemaGenerator` (`src/utils/forest-schema/`) builds the `.forestadmin-schema.json` apimap from the datasource; it is re-exported for the `agent-generator` package.

Expand Down
57 changes: 2 additions & 55 deletions packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
McpRouteMatcher,
WorkflowExecutorEmbedOptions,
} from './types';
import type { AiProviderDefinition } from '@forestadmin/agent-toolkit';
import type {
CollectionCustomizer,
DataSourceChartDefinition,
Expand Down Expand Up @@ -51,7 +50,6 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
protected nocodeCustomizer: DataSourceCustomizer<S>;
protected customizationService: CustomizationService;
protected schemaGenerator: SchemaGenerator;
protected aiProvider: AiProviderDefinition | null = null;

/** Whether MCP server should be mounted */
private mcpEnabled = false;
Expand Down Expand Up @@ -266,52 +264,6 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
return this;
}

/**
* Enable AI features for your Forest Admin panel.
*
* All AI requests from Forest Admin are forwarded to your agent and processed locally.
* Your data and API keys never transit through Forest Admin servers, ensuring full privacy.
*
* Requires the `@forestadmin/ai-proxy` package to be installed:
* ```bash
* npm install @forestadmin/ai-proxy
* ```
*
* @see {@link https://docs.forestadmin.com/developer-guide-agents-nodejs/agent-customization/ai/self-hosted-ai}
* @param provider - An AI provider definition created via `createAiProvider` from `@forestadmin/ai-proxy`
* @returns The agent instance for chaining
* @throws Error if addAi is called more than once
*
* @example
* import { createAiProvider } from '@forestadmin/ai-proxy';
*
* agent.addAi(createAiProvider({
* name: 'assistant',
* provider: 'openai',
* apiKey: process.env.OPENAI_API_KEY,
* model: 'gpt-4o',
* }));
*/
addAi(provider: AiProviderDefinition): this {
if (this.aiProvider) {
throw new Error(
'addAi can only be called once. Multiple AI configurations are not supported yet.',
);
}

this.aiProvider = provider;

for (const p of provider.providers) {
this.options.logger(
'Warn',
`AI configuration added with model '${p.model}'. ` +
'Make sure to test Forest Admin AI features thoroughly to ensure compatibility.',
);
}

return this;
}

/**
* Run a workflow executor in-process, alongside the agent. The agent boots it on start(),
* stops it on stop(), and proxies `/_internal/executor/*` to it — no separate deployment.
Expand Down Expand Up @@ -351,10 +303,7 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
}

protected getRoutes(dataSource: DataSource, services: ForestAdminHttpDriverServices) {
// init() is called on every start/restart to recreate routing state with a fresh Router.
const aiRouter = this.aiProvider?.init(this.options.logger) ?? null;

return makeRoutes(dataSource, this.options, services, aiRouter);
return makeRoutes(dataSource, this.options, services);
}

/**
Expand Down Expand Up @@ -462,9 +411,7 @@ export default class Agent<S extends TSchema = TSchema> extends FrameworkMounter
}

private buildSchemaMeta(): ForestSchema['meta'] {
const aiMeta = this.aiProvider?.providers ?? [];

return SchemaGenerator.buildMetadata(this.customizationService.buildFeatures(), aiMeta).meta;
return SchemaGenerator.buildMetadata(this.customizationService.buildFeatures()).meta;
}

private async buildRouterAndSendSchema(): Promise<{
Expand Down
40 changes: 0 additions & 40 deletions packages/agent/src/routes/ai/ai-proxy.ts

This file was deleted.

10 changes: 0 additions & 10 deletions packages/agent/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ForestAdminHttpDriverServices as Services } from '../services';
import type { AgentOptionsWithDefaults as Options } from '../types';
import type BaseRoute from './base-route';
import type { AiRouter } from '@forestadmin/agent-toolkit';
import type { DataSource } from '@forestadmin/datasource-toolkit';

import CollectionApiChartRoute from './access/api-chart-collection';
Expand All @@ -15,7 +14,6 @@ import Get from './access/get';
import List from './access/list';
import ListRelated from './access/list-related';
import NativeQueryDatasource from './access/native-query-datasource';
import AiProxyRoute from './ai/ai-proxy';
import Capabilities from './capabilities';
import ActionRoute from './modification/action/action';
import AssociateRelated from './modification/associate-related';
Expand Down Expand Up @@ -167,12 +165,6 @@ function getActionRoutes(
return routes;
}

function getAiRoutes(options: Options, services: Services, aiRouter: AiRouter | null): BaseRoute[] {
if (!aiRouter) return [];

return [new AiProxyRoute(services, options, aiRouter)];
}

function getWorkflowExecutorRoutes(options: Options, services: Services): BaseRoute[] {
if (!options.workflowExecutorUrl) return [];

Expand All @@ -183,7 +175,6 @@ export default function makeRoutes(
dataSource: DataSource,
options: Options,
services: Services,
aiRouter: AiRouter | null = null,
): BaseRoute[] {
const routes = [
...getRootRoutes(options, services),
Expand All @@ -193,7 +184,6 @@ export default function makeRoutes(
...getApiChartRoutes(dataSource, options, services),
...getRelatedRoutes(dataSource, options, services),
...getActionRoutes(dataSource, options, services),
...getAiRoutes(options, services, aiRouter),
...getWorkflowExecutorRoutes(options, services),
];

Expand Down
10 changes: 1 addition & 9 deletions packages/agent/src/utils/forest-schema/generator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { AgentOptionsWithDefaults } from '../../types';
import type { AiProviderMeta } from '@forestadmin/agent-toolkit';
import type { DataSource } from '@forestadmin/datasource-toolkit';
import type { ForestSchema } from '@forestadmin/forestadmin-client';

Expand All @@ -22,21 +21,14 @@ export default class SchemaGenerator {
};
}

static buildMetadata(
features: Record<string, string> | null,
aiProviders: AiProviderMeta[] = [],
): Pick<ForestSchema, 'meta'> {
static buildMetadata(features: Record<string, string> | null): Pick<ForestSchema, 'meta'> {
const { version } = require('../../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires,global-require

return {
meta: {
liana: 'forest-nodejs-agent',
liana_version: version,
liana_features: features,
ai_llms:
aiProviders.length > 0
? aiProviders.map(p => ({ name: p.name, provider: p.provider, model: p.model }))
: null,
stack: {
engine: 'nodejs',
engine_version: process.versions && process.versions.node,
Expand Down
Loading
Loading