Skip to content
Draft
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
18 changes: 17 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,18 @@ OPENAI_API_KEY=
GOOGLE_GENERATIVE_AI_API_KEY=

# ============ THIRD PARTY SERVICES ============
# Nango (for OAuth integrations)
# Nango (for MCP OAuth integrations)
NANGO_SECRET_KEY=
NANGO_SERVER_URL=http://localhost:3050
PUBLIC_NANGO_SERVER_URL=http://localhost:3050
PUBLIC_NANGO_CONNECT_BASE_URL=http://localhost:3051

# Nango for Slack App (separate from MCP integrations)
# Optional: Use a different Nango environment to isolate Slack auth from MCP auth
# If not set, falls back to NANGO_SECRET_KEY
# NANGO_SLACK_SECRET_KEY=
NANGO_SLACK_INTEGRATION_ID=slack-agent

SIGNOZ_URL=http://localhost:3080
PUBLIC_SIGNOZ_URL=http://localhost:3080
SIGNOZ_API_KEY=
Expand Down Expand Up @@ -94,3 +100,13 @@ PUBLIC_DISABLE_AUTH=true
# Get these from your GitHub App settings: https://github.com/settings/apps
# GITHUB_APP_ID=123456
# GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"

# ============ SLACK APP CONFIGURATION ============
# Get credentials from: https://api.slack.com/apps → Your App → Basic Information
# SLACK_CLIENT_ID=
# SLACK_CLIENT_SECRET=
# SLACK_SIGNING_SECRET=
# Your public URL for OAuth redirect (use ngrok for local dev)
# SLACK_APP_URL=https://your-app.ngrok.app
# UI URL for redirect after OAuth completes
# INKEEP_AGENTS_MANAGE_UI_URL=http://localhost:3000
100 changes: 100 additions & 0 deletions agents-api/__snapshots__/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9807,6 +9807,56 @@
]
}
},
"/manage/slack/install": {
"get": {
"description": "Redirects to Slack OAuth page for workspace installation",
"operationId": "slack-install",
"responses": {
"302": {
"description": "Redirect to Slack OAuth"
}
},
"summary": "Install Slack App",
"tags": [
"Work Apps",
"Slack"
]
}
},
"/manage/slack/oauth_redirect": {
"get": {
"description": "Handles the OAuth callback from Slack after workspace installation",
"operationId": "slack-oauth-redirect",
"parameters": [
{
"in": "query",
"name": "code",
"required": false,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "error",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"302": {
"description": "Redirect to dashboard with workspace data"
}
},
"summary": "Slack OAuth Callback",
"tags": [
"Work Apps",
"Slack"
]
}
},
"/manage/tenants/{tenantId}/playground/token": {
"post": {
"description": "Generates a short-lived API key (1 hour expiry) for authenticated users to access the run-api from the playground",
Expand Down Expand Up @@ -31777,6 +31827,56 @@
"MCP"
]
}
},
"/work-apps/slack/install": {
"get": {
"description": "Redirects to Slack OAuth page for workspace installation",
"operationId": "slack-install",
"responses": {
"302": {
"description": "Redirect to Slack OAuth"
}
},
"summary": "Install Slack App",
"tags": [
"Work Apps",
"Slack"
]
}
},
"/work-apps/slack/oauth_redirect": {
"get": {
"description": "Handles the OAuth callback from Slack after workspace installation",
"operationId": "slack-oauth-redirect",
"parameters": [
{
"in": "query",
"name": "code",
"required": false,
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "error",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"302": {
"description": "Redirect to dashboard with workspace data"
}
},
"summary": "Slack OAuth Callback",
"tags": [
"Work Apps",
"Slack"
]
}
}
},
"security": [
Expand Down
5 changes: 5 additions & 0 deletions agents-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@inkeep/agents-manage-mcp": "workspace:^",
"@inkeep/agents-mcp": "workspace:^",
"@modelcontextprotocol/sdk": "^1.25.2",
"@nangohq/node": "^0.69.20",
"@openrouter/ai-sdk-provider": "^1.2.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.64.1",
Expand All @@ -72,6 +73,8 @@
"@opentelemetry/sdk-node": "^0.205.0",
"@opentelemetry/sdk-trace-base": "^2.1.0",
"@opentelemetry/semantic-conventions": "^1.37.0",
"@slack/bolt": "^4.6.0",
"@slack/web-api": "^7.13.0",
"@vercel/functions": "^1.4.0",
"@vercel/sandbox": "^0.0.24",
"@workflow/builders": "4.0.1-beta.27",
Expand All @@ -90,6 +93,7 @@
"llm-info": "^1.0.69",
"openid-client": "^6.8.1",
"pg": "^8.16.3",
"slack-block-builder": "^2.8.0",
"workflow": "4.0.1-beta.33"
},
"peerDependencies": {
Expand All @@ -100,6 +104,7 @@
"@hono/vite-dev-server": "^0.20.1",
"@opentelemetry/exporter-trace-otlp-proto": "^0.203.0",
"@opentelemetry/sdk-metrics": "^2.1.0",
"@slack/types": "^2.19.0",
"@types/jmespath": "^0.15.2",
"@types/node": "^20.11.24",
"@types/pg": "^8.15.6",
Expand Down
4 changes: 4 additions & 0 deletions agents-api/src/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { githubRoutes } from './domains/github';
import { manageRoutes } from './domains/manage';
import mcpRoutes from './domains/mcp/routes/mcp';
import { runRoutes } from './domains/run';
import { workAppsRoutes } from './domains/work-apps';
import { env } from './env';
import { flushBatchProcessor } from './instrumentation';
import { getLogger } from './logger';
Expand Down Expand Up @@ -366,6 +367,9 @@ function createAgentsHono(config: AppConfig) {
// Mount GitHub routes - unauthenticated, OIDC token is the authentication
app.route('/api/github', githubRoutes);

// Mount Work Apps routes - modular third-party integrations (Slack, etc.)
app.route('/work-apps', workAppsRoutes);

// Mount MCP routes at top level (eclipses both manage and run services)
// Also available at /manage/mcp for backward compatibility
app.route('/mcp', mcpRoutes);
Expand Down
97 changes: 2 additions & 95 deletions agents-api/src/domains/evals/services/EvaluationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import manageDbPool from '../../../data/db/manageDbPool';
import runDbClient from '../../../data/db/runDbClient';
import { env } from '../../../env';
import { getLogger } from '../../../logger';
import { parseSSEResponse } from '../../../utils/sseParser';

const logger = getLogger('EvaluationService');

Expand Down Expand Up @@ -279,7 +280,7 @@ export class EvaluationService {
}

const responseText = await response.text();
const parseResult = this.parseSSEResponse(responseText);
const parseResult = parseSSEResponse(responseText);

// Check if the response indicates an error
if (parseResult.error) {
Expand Down Expand Up @@ -606,100 +607,6 @@ Generate the next user message:`;
return null;
}

/**
* Parse SSE (Server-Sent Events) response from chat API
* Handles text deltas, error operations, and other data operations
*/
private parseSSEResponse(sseText: string): { text: string; error?: string } {
let textContent = '';
let hasError = false;
let errorMessage = '';

const lines = sseText.split('\n').filter((line) => line.startsWith('data: '));

for (const line of lines) {
try {
const data = JSON.parse(line.slice(6)); // Remove 'data: ' prefix

// Handle OpenAI-compatible chat completion chunk format
if (data.object === 'chat.completion.chunk' && data.choices?.[0]?.delta) {
const delta = data.choices[0].delta;

// Extract text content
if (delta.content) {
textContent += delta.content;
}

// Check for embedded JSON in content (for operations)
if (delta.content && typeof delta.content === 'string') {
try {
const parsedContent = JSON.parse(delta.content);
if (parsedContent.type === 'data-operation' && parsedContent.data?.type === 'error') {
hasError = true;
errorMessage = parsedContent.data.message || 'Unknown error occurred';
logger.warn(
{
errorMessage,
errorData: parsedContent.data,
},
'Received error operation from chat API'
);
}
} catch {
// Not JSON, treat as regular text content
}
}
}
// Handle Vercel AI SDK data stream format
else if (data.type === 'text-delta' && data.delta) {
textContent += data.delta;
}
// Handle error operations (like the UI does)
else if (data.type === 'data-operation' && data.data?.type === 'error') {
hasError = true;
errorMessage = data.data.message || 'Unknown error occurred';
logger.warn(
{
errorMessage,
errorData: data.data,
},
'Received error operation from chat API'
);
}
// Handle error type directly
else if (data.type === 'error') {
hasError = true;
errorMessage = data.message || 'Unknown error occurred';
logger.warn(
{
errorMessage,
errorData: data,
},
'Received error event from chat API'
);
}
// Handle other response formats
else if (data.content) {
textContent +=
typeof data.content === 'string' ? data.content : JSON.stringify(data.content);
}
} catch {
// Skip invalid JSON lines (like '[DONE]' or empty lines)
}
}

if (hasError) {
return {
text: textContent.trim(),
error: errorMessage,
};
}

return {
text: textContent.trim(),
};
}

/**
* Run an evaluation job based on an evaluation job config
* Filters conversations based on jobFilters and runs evaluations with configured evaluators
Expand Down
1 change: 1 addition & 0 deletions agents-api/src/domains/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { createEvalRoutes, evalRoutes } from './evals';
export { createGithubRoutes, githubRoutes } from './github';
export { createManageRoutes, manageRoutes } from './manage';
export { createRunRoutes, runRoutes } from './run';
export { createWorkAppsRoutes, workAppsRoutes } from './work-apps';
6 changes: 6 additions & 0 deletions agents-api/src/domains/manage/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import type { ManageAppVariables } from '../../types/app';
import slackRoutes from '../work-apps/slack/routes';
import cliAuthRoutes from './routes/cliAuth';
import crudRoutes from './routes/index';
import invitationsRoutes from './routes/invitations';
Expand Down Expand Up @@ -39,6 +40,11 @@ export function createManageRoutes() {

app.route('/mcp', mcpRoutes);

// LEGACY: Mount Slack routes at old path for backwards compatibility
// Slack events/commands may still point to /manage/slack/*
// TODO: Update Slack app config to use /work-apps/slack/* and remove this
app.route('/slack', slackRoutes);

return app;
}

Expand Down
29 changes: 29 additions & 0 deletions agents-api/src/domains/work-apps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Work Apps Domain
*
* Modular integration layer for third-party work applications (Slack, GitHub, etc.)
* Designed to be easily extractable to a separate package/repo.
*
* Each work app is mounted as a sub-route:
* - /work-apps/slack/* - Slack workspace installation, user linking, commands
* - /work-apps/github/* - (future) GitHub integration
*/

import { OpenAPIHono } from '@hono/zod-openapi';
import slackRoutes from './slack/routes';
import type { WorkAppsVariables } from './types';

export function createWorkAppsRoutes() {
const app = new OpenAPIHono<{ Variables: WorkAppsVariables }>();

// Mount Slack routes - workspace installation, user linking, slash commands
app.route('/slack', slackRoutes);

// Future work apps can be mounted here:
// app.route('/github', githubWorkAppRoutes);
// app.route('/notion', notionRoutes);

return app;
}

export const workAppsRoutes = createWorkAppsRoutes();
Loading