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
53,455 changes: 53,455 additions & 0 deletions public/agent-index/docs-vector-store.json
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file was built based on MD files from tangleml.com

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions src/agent/agents/subagents/generalHelp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* General-help sub-agent — answers Tangle / docs / concept / best-practices
* questions by running the in-browser `search_docs` RAG tool and citing
* the matching documentation page back to the user.
*/
import { Agent } from "@openai/agents";

import { requireSubagentModel } from "../../config";
import { attachObservabilityHooks } from "../../middleware/observability";
import generalHelpPrompt from "../../prompts/generalHelp.md?raw";
import type { AgentSession } from "../../session";
import { createSearchDocsTool } from "../../tools/searchDocs";

export function createGeneralHelpAgent(session: AgentSession): Agent {
const agent = new Agent({
name: "general-help",
handoffDescription: `Answer general questions about Tangle concepts, features, best practices,
and product behavior. Not specific to the current pipeline.`,
instructions: generalHelpPrompt,
tools: [createSearchDocsTool(session.proxyClient)],
model: requireSubagentModel(),
});
attachObservabilityHooks(agent, session.emitStatus);
return agent;
}
32 changes: 19 additions & 13 deletions src/agent/agents/tangleDispatcher.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
/**
* Top-level dispatcher agent for the in-browser AI assistant.
*
* The dispatcher itself does not perform end-user tasks. It classifies
* the user's intent and hands off to the specialist sub-agent registered
* for that intent. Each sub-agent is session-scoped, so the dispatcher Agent is rebuilt on
* every turn.
*/
import { Agent, MemorySession, run } from "@openai/agents";
import { RECOMMENDED_PROMPT_PREFIX } from "@openai/agents-core/extensions";

import { ensureProxyConfigured, requireOrchestratorModel } from "../config";
import { requireOrchestratorModel } from "../config";
import { attachObservabilityHooks } from "../middleware/observability";
import dispatcherPrompt from "../prompts/dispatcher.md?raw";
import type { AgentSession } from "../session";
import type { StatusCallback } from "../types";
import { createGeneralHelpAgent } from "./subagents/generalHelp";

interface DispatcherInvokeParams {
message: string;
Expand All @@ -25,21 +31,20 @@ export interface TangleDispatcher {
invoke(params: DispatcherInvokeParams): Promise<DispatcherInvokeResult>;
}

export function createDispatcher({
emitStatus,
}: {
emitStatus: StatusCallback;
}): TangleDispatcher {
const sessions = new Map<string, MemorySession>();

function createDispatcherAgent(session: AgentSession): Agent {
const agent = Agent.create({
name: "tangle-dispatcher",
model: requireOrchestratorModel(),
instructions: dispatcherPrompt,
instructions: `${RECOMMENDED_PROMPT_PREFIX}\n\n${dispatcherPrompt}`,
tools: [],
handoffs: [],
handoffs: [createGeneralHelpAgent(session)],
});
attachObservabilityHooks(agent, emitStatus);
attachObservabilityHooks(agent, session.emitStatus);
return agent;
}

export function createDispatcher(): TangleDispatcher {
const sessions = new Map<string, MemorySession>();

function getOrCreateSessionMemory(threadId: string): MemorySession {
const existing = sessions.get(threadId);
Expand All @@ -51,8 +56,9 @@ export function createDispatcher({

return {
async invoke(params) {
ensureProxyConfigured(params.token);
params.session.proxyClient.ensureConfigured(params.token);
const sessionMemory = getOrCreateSessionMemory(params.threadId);
const agent = createDispatcherAgent(params.session);
const result = await run(agent, params.message, {
session: sessionMemory,
});
Expand Down
107 changes: 74 additions & 33 deletions src/agent/config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
/**
* Configuration and one-time setup for the in-browser agent.
* Configuration and proxy-client wiring for the in-browser agent.
*
* Static values (proxy URL, model names, mode) come from the committed
* `src/config/aiAssistantConfig.json` file. The secret proxy token is
* NOT read here — the worker reads it from IndexedDB (via
* `src/agent/aiTokenStore.ts`, shared with the main thread) on every
* `ask()` turn and passes the resolved value into
* `ensureProxyConfigured(token)`, which re-builds the OpenAI client
* when the token rotates.
* The single `ProxyClient` instance is owned by the worker (see
* `src/agent/worker.ts`) and threaded into every `AgentSession` so
* tools (e.g. `searchDocs`) can read the configured client without
* touching module-global state.
*
* `proxyMode` exists so a future PR can flip the runtime from
* `"browser-direct"` (current beta) to `"backend-proxy"` without a
* rewrite. Only `"browser-direct"` is implemented in this PR.
* rewrite.
*/
import {
setDefaultOpenAIClient,
Expand All @@ -27,6 +24,7 @@ export const config = aiAssistantConfig as {
proxyMode: "browser-direct" | "backend-proxy";
orchestratorModel: string;
subagentModel: string;
embeddingModel: string;
};

function requireProxyBaseUrl(): string {
Expand All @@ -47,40 +45,83 @@ export function requireOrchestratorModel(): string {
return config.orchestratorModel;
}

let lastConfiguredToken: string | null = null;
export function requireSubagentModel(): string {
if (!config.subagentModel) {
throw new Error(
"AI assistant: subagentModel is empty. Set it in src/config/aiAssistantConfig.json.",
);
}
return config.subagentModel;
}

export function requireEmbeddingModel(): string {
if (!config.embeddingModel) {
throw new Error(
"AI assistant: embeddingModel is empty. Set it in src/config/aiAssistantConfig.json.",
);
}
return config.embeddingModel;
}

/**
* Read-only seam used by tools (e.g. `searchDocs`) that need the
* configured `OpenAI` client. `ProxyClient` implements this; tests can
* provide a duck-typed substitute without instantiating the class.
*/
export interface OpenAIProvider {
readonly openai: OpenAI;
}

/**
* Wires the configured LLM proxy as the default OpenAI client for
* `@openai/agents`. Called once per turn from the dispatcher with the
* current token. Re-builds the client when the token rotates; otherwise
* a no-op.
* Owns the lifecycle of the configured `OpenAI` client for the
* in-browser agent. A single instance is allocated by the worker and
* threaded through every `AgentSession`.
*
* `ensureConfigured(token)` is called once per turn from the
* dispatcher and re-builds the underlying client when the token
* rotates; otherwise it is a no-op. The `openai` getter exposes that
* same client to worker-side tools that need direct API access (e.g.
* `embeddings.create`) without going through the agent SDK runtime.
*
* - `setOpenAIAPI("chat_completions")`: the proxy exposes Chat
* Completions, not the OpenAI Responses API.
* - `setTracingDisabled(true)`: the SDK's default tracing exporter
* would POST to `api.openai.com`, which is unreachable through the
* proxy.
*/
export function ensureProxyConfigured(token: string): void {
if (config.proxyMode === "backend-proxy") {
throw new Error(
"AI assistant: backend-proxy mode is not implemented yet. Set proxyMode to 'browser-direct' in src/config/aiAssistantConfig.json.",
);
}
if (!token) {
throw new Error(
"AI assistant: missing proxy token. Set it via the AI panel.",
);
}
if (lastConfiguredToken === token) return;
setDefaultOpenAIClient(
new OpenAI({
export class ProxyClient implements OpenAIProvider {
#client: OpenAI | null = null;
#lastToken: string | null = null;

ensureConfigured(token: string): void {
if (config.proxyMode === "backend-proxy") {
throw new Error(
"AI assistant: backend-proxy mode is not implemented yet. Set proxyMode to 'browser-direct' in src/config/aiAssistantConfig.json.",
);
}
if (!token) {
throw new Error(
"AI assistant: missing proxy token. Set it via the AI panel.",
);
}
if (this.#lastToken === token && this.#client) return;
this.#client = new OpenAI({
apiKey: token,
baseURL: requireProxyBaseUrl(),
dangerouslyAllowBrowser: true,
}),
);
setOpenAIAPI("chat_completions");
setTracingDisabled(true);
lastConfiguredToken = token;
});
setDefaultOpenAIClient(this.#client);
setOpenAIAPI("chat_completions");
setTracingDisabled(true);
this.#lastToken = token;
}

get openai(): OpenAI {
if (!this.#client) {
throw new Error(
"AI assistant: OpenAI client is not configured. proxyClient.ensureConfigured(token) must run first (the dispatcher does this on every turn).",
);
}
return this.#client;
}
}
45 changes: 45 additions & 0 deletions src/agent/idb/agentDb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Dexie, type EntityTable } from "dexie";

export const TANGLE_ML_DOCS_VECTORS_KEY = "tangle-ml-documentation";

/**
* One persisted vector: the source chunk text, its embedding, and any
* metadata the producer wants to round-trip (title, url, section, ...).
* Metadata is `unknown` at the IDB layer because different vector
* stores carry different metadata shapes; consumers cast to their
* domain-specific type on read.
*/
export interface PersistedVector {
content: string;
embedding: number[];
metadata: Record<string, unknown>;
}

export interface PersistedVectorStore {
version: number;
embeddingModel: string;
vectors: PersistedVector[];
}

export interface VectorStoreEntry {
key: string;
embeddingModel: string;
version: number;
payload: PersistedVectorStore;
}

export interface SkillEntry {
id: string;
version: string;
content: string;
}

export const agentDb = new Dexie("tangle_agent") as Dexie & {
vectors: EntityTable<VectorStoreEntry, "key">;
skills: EntityTable<SkillEntry, "id">;
};

agentDb.version(1).stores({
vectors: "key",
skills: "id",
});
42 changes: 17 additions & 25 deletions src/agent/prompts/dispatcher.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,36 @@
# Tangle Assistant — System Prompt
# Tangle Dispatcher — System Prompt

You are the **Tangle Assistant**, an AI helper for Tangle Pipeline Studio. Your job in this release is to answer questions about Tangle, ML pipelines, and how to use the product.
You are the **Tangle Dispatcher**, the entry point for the Tangle Pipeline Studio AI assistant. Your job is to classify the user's intent and hand off to the right specialist. You do not answer Tangle questions yourself.

## What you can do today
## Available specialists

- Explain Tangle concepts (pipelines, tasks, components, runs, executions, inputs/outputs, subgraphs, etc.).
- Discuss ML pipeline patterns and best practices at a general level.
- Suggest approaches the user could take in Tangle Pipeline Studio.
| Specialist | When to hand off |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `general-help` | Any question about Tangle concepts, features, how things work, best practices, getting started, or documentation lookups (e.g. "what is a pipeline?", "how do I connect tasks?", "what are subgraphs?"). |

## What you cannot do today
Future releases will add specialists for building, fixing, and debugging pipelines. Until then, hand off **every** Tangle / pipeline / ML / docs question to `general-help`.

- You **cannot** inspect the user's current pipeline. You have no access to its tasks, connections, arguments, or YAML.
- You **cannot** make changes to the pipeline. You have no tools that mutate the editor state.
- You **cannot** run pipelines, fetch run logs, or look up execution status.
## Your workflow

If the user asks for any of the above, be upfront: explain that those abilities are not available yet, and offer what you can — for example, a conceptual explanation, a checklist, or pseudocode they can apply themselves.

## Off-topic handling

If the user asks something unrelated to Tangle, ML pipelines, or data workflows, respond briefly and politely:

> "I'm the Tangle Assistant — I can help with pipeline concepts and how to use Tangle. That question is outside what I can help with today."

Do not attempt to answer off-topic questions.
1. Read the user's message.
2. If it is a Tangle / pipeline / ML / docs question, hand off to `general-help` using its handoff tool. The specialist will produce the final response — do not announce the hand-off to the user.
3. If it is off-topic (weather, jokes, general coding help unrelated to Tangle, etc.), respond directly with a brief polite message such as:
> "I'm the Tangle Assistant — I can help with Tangle concepts and how to use the product. That question is outside what I can help with today."
> Do not hand off off-topic questions.
4. If you genuinely cannot tell whether a question is Tangle-related, ask one brief clarifying question instead of guessing.

## Response formatting

Future releases will surface entities and components as interactive chips in the chat panel via these markdown link formats:
Specialists may emit special markdown link formats that the UI renders as interactive chips:

```
[Entity Name](entity://$id)
[Component Name](component://component-id)
```

If you ever reference a specific entity or component by id, use those link formats verbatimdo not rewrite them as bold, italic, or backticks. Today you do not have a tool to look up real ids, so only emit these links when the user has already mentioned an id explicitly.
When you do respond directly (off-topic only), keep any such links intactnever rewrite them as bold, italic, or backticks. Don't invent ids.

## Style

- Be brief and natural. Aim for a few short paragraphs or a short list, not a wall of text.
- Use plain language. Define jargon when you introduce it.
- When you give steps, number them.
- When code is helpful, use fenced code blocks with an explicit language tag.
- Be brief and natural when you respond directly.
- Never apologize for limitations more than once per turn — state them plainly and move on.
45 changes: 45 additions & 0 deletions src/agent/prompts/generalHelp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# General Help — System Prompt

You are the **General Help** assistant for Tangle Pipeline Studio. Your job is to answer questions about Tangle concepts, features, best practices, and product behavior.

## Your Knowledge Areas

- **Pipeline concepts**: What pipelines are, how they work, DAG structure, stages, subgraphs.
- **Components**: What components are, how they define inputs/outputs, how they're referenced by tasks.
- **Inputs & Outputs**: Pipeline-level I/O, how data flows between tasks via bindings.
- **Bindings**: How connections work, port-to-port wiring, type compatibility.
- **Subgraphs**: What they are, when to use them, how they encapsulate groups of tasks.
- **Validation**: What the validator checks (schema, types, cycles, required inputs).
- **Execution**: How pipeline runs work, task execution order, artifacts.
- **Best practices**: Pipeline design patterns, component reuse, naming conventions.

## Using Documentation Search

You have access to `search_docs` to look up official Tangle documentation. **Always call `search_docs` first** for any question about Tangle — how things work, what features exist, how to get started, etc.

### MANDATORY: Include Documentation Links

Every response that uses information from `search_docs` **MUST** include a link to the documentation page. This is not optional.

Each search result contains a `url` and a pre-formatted `citation` field. Use them like this:

> Learn more: [What are Components?](https://tangleml.com/docs/core-concepts/what-are-components)

Rules:

- **Always** place at least one documentation link in your response.
- If your answer draws from multiple doc pages, include a link for each.
- Place links inline or at the end of relevant paragraphs — do not bury them.
- Use the `citation` field from the search results directly when possible.

## What You CANNOT Do

- Modify pipelines or run executions.
- Access the current pipeline state (you answer general questions, not pipeline-specific ones).
- Answer questions unrelated to Tangle or ML pipelines.

If the user asks about their specific pipeline, suggest they ask about it directly so the appropriate specialist can help in a future release.

## Response Style

Be informative and concise. Use examples when they help clarify concepts. Ground your answers in official documentation whenever possible. If you're unsure about a specific product detail, say so rather than guessing.
Loading
Loading