Skip to content
Closed
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
9 changes: 9 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { getAccessToken } from "./utils/auth-utils.js";
import { createFunctionsModule } from "./modules/functions.js";
import { createAgentsModule } from "./modules/agents.js";
import { createDynamicAgentsModule } from "./modules/dynamic-agents.js";
import { createAppLogsModule } from "./modules/app-logs.js";
import { createUsersModule } from "./modules/users.js";
import { RoomsSocket, RoomsSocketConfig } from "./utils/socket-utils.js";
Expand Down Expand Up @@ -190,6 +191,10 @@ export function createClient(config: CreateClientConfig): Base44Client {
serverUrl,
token,
}),
dynamicAgents: createDynamicAgentsModule({
serverUrl,
getToken: () => token || getAccessToken() || undefined,
}),
appLogs: createAppLogsModule(axiosClient, appId),
users: createUsersModule(axiosClient, appId),
analytics: createAnalyticsModule({
Expand Down Expand Up @@ -233,6 +238,10 @@ export function createClient(config: CreateClientConfig): Base44Client {
serverUrl,
token,
}),
dynamicAgents: createDynamicAgentsModule({
serverUrl,
getToken: () => serviceToken,
}),
appLogs: createAppLogsModule(serviceRoleAxiosClient, appId),
cleanup: () => {
if (socket) {
Expand Down
5 changes: 5 additions & 0 deletions src/client.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { FunctionsModule } from "./modules/functions.types.js";
import type { AgentsModule } from "./modules/agents.types.js";
import type { AppLogsModule } from "./modules/app-logs.types.js";
import type { AnalyticsModule } from "./modules/analytics.types.js";
import type { DynamicAgentsModule } from "./modules/dynamic-agents.types.js";

/**
* Options for creating a Base44 client.
Expand Down Expand Up @@ -91,6 +92,8 @@ export interface Base44Client {
analytics: AnalyticsModule;
/** {@link AppLogsModule | App logs module} for tracking app usage. */
appLogs: AppLogsModule;
/** {@link DynamicAgentsModule | Dynamic agents module} for code-defined AI agents and tool loops. */
dynamicAgents: DynamicAgentsModule;
/** {@link AuthModule | Auth module} for user authentication and management. */
auth: AuthModule;
/** {@link UserConnectorsModule | Connectors module} for app-user OAuth flows. */
Expand Down Expand Up @@ -131,6 +134,8 @@ export interface Base44Client {
agents: AgentsModule;
/** {@link AppLogsModule | App logs module} with elevated permissions. */
appLogs: AppLogsModule;
/** {@link DynamicAgentsModule | Dynamic agents module} with elevated permissions. */
dynamicAgents: DynamicAgentsModule;
/** {@link ConnectorsModule | Connectors module} for OAuth token retrieval. */
connectors: ConnectorsModule;
/** {@link EntitiesModule | Entities module} with elevated permissions. */
Expand Down
17 changes: 17 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
removeAccessToken,
getLoginUrl,
} from "./utils/auth-utils.js";
import { tool } from "./modules/dynamic-agents.js";

export {
createClient,
Expand All @@ -21,6 +22,7 @@ export {
saveAccessToken,
removeAccessToken,
getLoginUrl,
tool,
};

export type {
Expand Down Expand Up @@ -115,6 +117,21 @@ export type {
CustomIntegrationCallResponse,
} from "./modules/custom-integrations.types.js";

export type {
Tool,
JSONSchema,
ChatMessage,
Step,
RunUsage,
RunResult,
RunInput,
RunOptions,
ToolChoice,
AgentConfig,
Agent,
DynamicAgentsModule,
} from "./modules/dynamic-agents.types.js";

// Auth utils types
export type {
GetAccessTokenOptions,
Expand Down
58 changes: 58 additions & 0 deletions src/modules/ai-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Base44Error } from "../utils/axios-client.js";
import type { DynamicAgentsModuleConfig } from "./dynamic-agents.types.js";

/**
* Resolves the AI Gateway connection from a client config.
* @internal
*/
export function resolveConnection(config: DynamicAgentsModuleConfig): {
baseURL: string;
apiKey: string;
} {
const { serverUrl, getToken } = config;
// No appId in the path: the gateway resolves the app by request Host.
return {
baseURL: `${serverUrl}/api/ai/unified/v1`,
apiKey: getToken() ?? "",
};
}

/**
* Creates the gateway transport. Owns the single HTTP call to the OpenAI-compatible
* `/chat/completions` endpoint. Shaped so a streaming `.stream()` method can be added
* later without changing callers of `.complete()`.
* @internal
*/
export function createGatewayTransport(config: DynamicAgentsModuleConfig) {
return {
async complete(
body: Record<string, unknown>,
opts: { signal?: AbortSignal } = {}
): Promise<any> {
const { baseURL, apiKey } = resolveConnection(config);
const res = await fetch(`${baseURL}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(body),
signal: opts.signal,
});

const json = await res.json().catch(() => null);

if (!res.ok) {
const err = (json && json.error) || {};
throw new Base44Error(
err.message || `AI Gateway request failed with status ${res.status}`,
res.status,
err.code || err.type || "ai_gateway_error",
json,
null
);
}
return json;
},
};
}
204 changes: 204 additions & 0 deletions src/modules/dynamic-agents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { createGatewayTransport } from "./ai-gateway.js";
import type {
Agent,
AgentConfig,
ChatMessage,
DynamicAgentsModule,
DynamicAgentsModuleConfig,
RunInput,
RunOptions,
RunResult,
Step,
Tool,
} from "./dynamic-agents.types.js";

/**
* Defines a tool an agent can call.
*
* @example
* ```typescript
* const getWeather = tool({
* description: "Get the current weather for a city.",
* parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
* execute: async ({ city }) => ({ city, tempC: 28 }),
* });
* ```
*/
export function tool(t: Tool): Tool {
return t;
}

/**
* Maps a `{ name: Tool }` map to the OpenAI `tools[]` array. Returns `undefined`
* when empty so the param is omitted from the request body.
* @internal
*/
export function serializeTools(tools?: Record<string, Tool>): any[] | undefined {
if (!tools) return undefined;
const entries = Object.entries(tools);
if (entries.length === 0) return undefined;
return entries.map(([name, t]) => ({
type: "function",
function: { name, description: t.description, parameters: t.parameters },
}));
}

/**
* Builds the gateway request body from config + messages using an explicit whitelist.
* Rejected params (max_tokens, stop, top_p, penalties, logit_bias, seed, n) can never
* appear because only the supported keys are ever written.
* @internal
*/
export function buildRequestBody(
config: AgentConfig,
messages: ChatMessage[]
): Record<string, unknown> {
const body: Record<string, unknown> = {
model: config.model,
messages,
};
if (config.temperature !== undefined) body.temperature = config.temperature;
if (config.toolChoice !== undefined) body.tool_choice = config.toolChoice;
if (config.responseFormat !== undefined) {
body.response_format = {
type: "json_schema",
json_schema: { name: "response", schema: config.responseFormat, strict: true },
};
}
const tools = serializeTools(config.tools);
if (tools) body.tools = tools;
return body;
}

const DEFAULT_MAX_STEPS = 8;

function inputToMessages(input: RunInput): ChatMessage[] {
if ("messages" in input) return input.messages;
return [{ role: "user", content: input.prompt }];
}

function stringifyResult(out: unknown): string {
return typeof out === "string" ? out : JSON.stringify(out);
}

function mapUsage(raw: any): RunResult["usage"] {
const u = (raw && raw.usage) || {};
return {
promptTokens: u.prompt_tokens,
completionTokens: u.completion_tokens,
totalTokens: u.total_tokens,
credits: u.base44_credits,
};
}

/**
* Creates the `base44.dynamicAgents` module.
* @internal
*/
export function createDynamicAgentsModule(
config: DynamicAgentsModuleConfig
): DynamicAgentsModule {
const transport = createGatewayTransport(config);

function create(agentConfig: AgentConfig): Agent {
const maxSteps = agentConfig.maxSteps ?? DEFAULT_MAX_STEPS;
const tools = agentConfig.tools;

const agent: Agent = {
async run(input: RunInput, options: RunOptions = {}): Promise<RunResult> {
const messages: ChatMessage[] = [];
if (agentConfig.system) messages.push({ role: "system", content: agentConfig.system });
messages.push(...inputToMessages(input));

const steps: Step[] = [];
let raw: any = null;

for (let i = 0; i < maxSteps; i++) {
const body = buildRequestBody(agentConfig, messages);
raw = await transport.complete(body, { signal: options.abortSignal });

const choice = raw?.choices?.[0];
const message = choice?.message ?? { role: "assistant", content: "" };
messages.push(message);

const toolCalls = message.tool_calls;
if (!toolCalls || toolCalls.length === 0) {
return {
text: message.content ?? "",
steps,
finishReason: choice?.finish_reason ?? "stop",
usage: mapUsage(raw),
raw,
};
}

const toolResults: Step["toolResults"] = [];
for (const call of toolCalls) {
const name = call.function?.name;
const t = tools?.[name];
let args: unknown = {};
try {
args = JSON.parse(call.function?.arguments || "{}");
} catch {
args = {};
}
let resultContent: string;
if (!t) {
resultContent = `Error: tool "${name}" is not available.`;
} else {
try {
resultContent = stringifyResult(await t.execute(args));
} catch (e: any) {
resultContent = `Error: ${e?.message ?? String(e)}`;
}
}
messages.push({ role: "tool", tool_call_id: call.id, content: resultContent });
toolResults.push({ toolCallId: call.id, toolName: name, args, result: resultContent });
}
steps.push({ toolResults });
}

// maxSteps exhausted
const lastMessage = raw?.choices?.[0]?.message;
return {
text: lastMessage?.content ?? "",
steps,
finishReason: "max_steps",
usage: mapUsage(raw),
raw,
};
},

asTool(toolOpts: { name?: string; description: string }): Tool {
return {
description: toolOpts.description,
parameters: {
type: "object",
properties: { prompt: { type: "string", description: "What to ask the sub-agent." } },
required: ["prompt"],
},
execute: async (args: { prompt: string }) => {
const result = await agent.run({ prompt: args.prompt });
return result.text;
},
};
},
};

return agent;
}

function run(
runConfig: AgentConfig & ({ prompt: string } | { messages: ChatMessage[] }),
options?: RunOptions
): Promise<RunResult> {
if ("messages" in runConfig) {
const { messages, ...agentConfig } = runConfig as AgentConfig & { messages: ChatMessage[] };
return create(agentConfig).run({ messages }, options);
}
const { prompt, ...agentConfig } = runConfig as AgentConfig & { prompt: string };
return create(agentConfig).run({ prompt }, options);
}

return { create, run };
}
Loading
Loading