Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
NAA_ENV=development
NAA_DATABASE_PATH=naa.sqlite

# LangSmith / LangGraph tracing
# Set LANGSMITH_TRACING=true and LANGSMITH_API_KEY to trace graph runs.
LANGSMITH_TRACING=false
LANGSMITH_API_KEY=
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_PROJECT=narrative-alpha-agent
LANGSMITH_WORKSPACE_ID=
LANGSMITH_RUN_NAME=narrative-alpha-agent
LANGSMITH_TAGS=naa,langgraph,narrative-replay

# LLM provider selection.
# Supported values:
# local, openai, anthropic, deepseek, openrouter, google, cohere, mistral,
Expand Down Expand Up @@ -79,3 +89,8 @@ TWITTER_BEARER_TOKEN=
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
NEWS_API_KEY=

# Alert notifiers
DISCORD_WEBHOOK_URL=
DISCORD_USERNAME=Narrative Alpha Agent
DISCORD_AVATAR_URL=
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ pnpm run test
- [Backtesting](docs/BACKTESTING.md)
- [Operations](docs/OPERATIONS.md)
- [Providers and Secrets](docs/PROVIDERS.md)
- [Observability](docs/OBSERVABILITY.md)
- [Notifications](docs/NOTIFICATIONS.md)
- [Contributing](CONTRIBUTING.md)
- [Security](SECURITY.md)
- [Changelog](CHANGELOG.md)
Expand Down
42 changes: 42 additions & 0 deletions docs/NOTIFICATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Notifications

Narrative Alpha Agent emits alerts through the `Notifier` port in `src/types/ports.ts`.

## Built-in Notifiers

- `ConsoleNotifier`: writes alerts to stdout
- `TelegramNotifierStub`: captures alert messages for future Telegram integration
- `DiscordNotifier`: sends rich embeds to a Discord webhook

## Discord

Set these variables in `.env`:

```bash
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
DISCORD_USERNAME=Narrative Alpha Agent
DISCORD_AVATAR_URL=
```

When `DISCORD_WEBHOOK_URL` is unset, Discord is not enabled. This keeps tests and replay offline by default.

Discord alerts include:

- narrative name
- alert reason
- NIP score
- lifecycle state
- document count
- timestamp

## Adding More Notifiers

Implement `Notifier`:

```ts
interface Notifier {
notify(narrative: Narrative, message: string): Promise<void>;
}
```

Then inject it through `createRuntime({ notifiers: [...] })` or add a typed environment-backed config in `src/config/notifications.ts`.
55 changes: 55 additions & 0 deletions docs/OBSERVABILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Observability

Narrative Alpha Agent uses LangGraph as its execution layer, so graph runs can be traced with LangSmith through LangChain's standard tracing environment variables.

Tracing is disabled by default to keep local development and replay offline.

## Enable LangSmith

Copy `.env.example` to `.env` and set:

```bash
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=lsv2_...
LANGSMITH_PROJECT=narrative-alpha-agent
LANGSMITH_RUN_NAME=narrative-alpha-agent
LANGSMITH_TAGS=naa,langgraph,narrative-replay
```

Optional:

```bash
LANGSMITH_ENDPOINT=https://api.smith.langchain.com
LANGSMITH_WORKSPACE_ID=...
```

The runtime reads these values in `src/config/observability.ts`.

## What Gets Traced

`NarrativeGraphRunner` passes LangChain runnable config into every graph invocation:

- `runName`
- `tags`
- metadata containing app name, replay timestamp, document count, project, and tracing status
- `thread_id` for LangGraph checkpointing and replay re-entry

When LangSmith tracing is enabled, LangGraph/LangChain records the graph run and child node spans under the configured project.

## Replay Tracing

Replay uses a timestamp-specific thread ID:

```text
replay-1735689600000
replay-1735693200000
...
```

This makes historical checkpoints searchable in LangSmith by run metadata and thread ID.

## Local Safety

- No traces are emitted unless `LANGSMITH_TRACING=true`.
- No LangSmith API key is required for tests, replay, or local development.
- Do not commit real `LANGSMITH_API_KEY` values.
11 changes: 11 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ The default runtime uses:

- `ConsoleNotifier`
- `TelegramNotifierStub`
- `DiscordNotifier` when `DISCORD_WEBHOOK_URL` is configured

Real notifier implementations should implement `Notifier` and be wired through runtime construction.

See [Notifications](NOTIFICATIONS.md).

## LLM and Embeddings

The default MVP path does not require external paid APIs.
Expand All @@ -45,6 +48,14 @@ LLM credentials are configured with `.env.example`. The provider registry suppor

The deterministic local embedding provider is used by default for replay stability.

## Observability

LangSmith tracing is configured through `.env.example`.

Set `LANGSMITH_TRACING=true`, `LANGSMITH_API_KEY`, and `LANGSMITH_PROJECT` to trace LangGraph runs. Runtime metadata includes replay timestamp, document count, tags, project, and thread ID.

See [Observability](OBSERVABILITY.md).

## CI

GitHub Actions runs:
Expand Down
2 changes: 2 additions & 0 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,5 @@ Real source connectors should implement `DocumentSourceConnector` from `src/type
- Scope keys to the minimum provider permissions.
- Rotate keys after accidental disclosure.
- Keep production secrets in a secret manager, not in local files.

LangSmith and notifier secrets are also listed in `.env.example`.
63 changes: 51 additions & 12 deletions src/agents/createRuntime.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { defaultConfig, type AppConfig } from "../config/defaults.js";
import { readNotificationConfigFromEnv, type NotificationConfig } from "../config/notifications.js";
import { readLangSmithConfigFromEnv, type LangSmithConfig } from "../config/observability.js";
import { readProviderConfigFromEnv, type ProviderSecretConfig } from "../config/secrets.js";
import { SqliteNarrativeRepository } from "../db/SqliteNarrativeRepository.js";
import { NarrativeGraphRunner } from "../graph/agentGraph.js";
import { AlertService, ConsoleNotifier, TelegramNotifierStub } from "../services/AlertService.js";
import {
AlertService,
ConsoleNotifier,
DiscordNotifier,
TelegramNotifierStub
} from "../services/AlertService.js";
import { ClusteringService } from "../services/ClusteringService.js";
import { DeterministicEmbeddingProvider } from "../services/DeterministicEmbeddingProvider.js";
import { InMemoryVectorStore } from "../services/InMemoryVectorStore.js";
Expand All @@ -11,13 +18,17 @@ import { NarrativeLifecycleService } from "../services/NarrativeLifecycleService
import { ScoringService } from "../services/ScoringService.js";
import { SentimentService } from "../services/SentimentService.js";
import type { NarrativeRepository } from "../types/ports.js";
import type { Notifier } from "../types/ports.js";

export type RuntimeOptions = {
config?: Partial<AppConfig>;
repository?: NarrativeRepository;
databasePath?: string;
enableConsoleAlerts?: boolean;
llmProvider?: ProviderSecretConfig;
notifications?: NotificationConfig;
langSmith?: LangSmithConfig;
notifiers?: Notifier[];
};

export const createRuntime = (options: RuntimeOptions = {}): NarrativeGraphRunner => {
Expand All @@ -29,6 +40,7 @@ export const createRuntime = (options: RuntimeOptions = {}): NarrativeGraphRunne
...options.config?.scoringWeights
}
};
const langSmith = options.langSmith ?? readLangSmithConfigFromEnv();
const repository =
options.repository ?? new SqliteNarrativeRepository(options.databasePath ?? "naa.sqlite");
repository.initialize();
Expand All @@ -44,18 +56,45 @@ export const createRuntime = (options: RuntimeOptions = {}): NarrativeGraphRunne
});
void llm;
const scoring = new ScoringService(config.scoringWeights);
const notifications = options.notifications ?? readNotificationConfigFromEnv();
const notifiers =
options.enableConsoleAlerts === false
? [new TelegramNotifierStub()]
: [new ConsoleNotifier(), new TelegramNotifierStub()];
options.notifiers ??
buildNotifiers({
enableConsoleAlerts: options.enableConsoleAlerts,
notifications
});
const alerts = new AlertService(notifiers);

return new NarrativeGraphRunner({
config,
clustering,
lifecycle,
scoring,
repository,
alerts
});
return new NarrativeGraphRunner(
{
config,
clustering,
lifecycle,
scoring,
repository,
alerts
},
langSmith
);
};

const buildNotifiers = (options: {
enableConsoleAlerts?: boolean | undefined;
notifications: NotificationConfig;
}): Notifier[] => {
const notifiers: Notifier[] = [];
if (options.enableConsoleAlerts !== false) {
notifiers.push(new ConsoleNotifier());
}
notifiers.push(new TelegramNotifierStub());
if (options.notifications.discordWebhookUrl) {
notifiers.push(
new DiscordNotifier({
webhookUrl: options.notifications.discordWebhookUrl,
username: options.notifications.discordUsername,
avatarUrl: options.notifications.discordAvatarUrl
})
);
}
return notifiers;
};
13 changes: 13 additions & 0 deletions src/config/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type NotificationConfig = {
discordWebhookUrl?: string | undefined;
discordUsername?: string | undefined;
discordAvatarUrl?: string | undefined;
};

export const readNotificationConfigFromEnv = (
env: NodeJS.ProcessEnv = process.env
): NotificationConfig => ({
discordWebhookUrl: env.DISCORD_WEBHOOK_URL,
discordUsername: env.DISCORD_USERNAME ?? "Narrative Alpha Agent",
discordAvatarUrl: env.DISCORD_AVATAR_URL
});
27 changes: 27 additions & 0 deletions src/config/observability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type LangSmithConfig = {
tracingEnabled: boolean;
apiKey?: string | undefined;
endpoint?: string | undefined;
project?: string | undefined;
workspaceId?: string | undefined;
runName: string;
tags: string[];
};

export const readLangSmithConfigFromEnv = (
env: NodeJS.ProcessEnv = process.env
): LangSmithConfig => ({
tracingEnabled: env.LANGSMITH_TRACING === "true" || env.LANGCHAIN_TRACING_V2 === "true",
apiKey: env.LANGSMITH_API_KEY,
endpoint: env.LANGSMITH_ENDPOINT ?? "https://api.smith.langchain.com",
project: env.LANGSMITH_PROJECT ?? "narrative-alpha-agent",
workspaceId: env.LANGSMITH_WORKSPACE_ID,
runName: env.LANGSMITH_RUN_NAME ?? "narrative-alpha-agent",
tags: parseTags(env.LANGSMITH_TAGS ?? "naa,langgraph,narrative-replay")
});

const parseTags = (value: string): string[] =>
value
.split(",")
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
19 changes: 18 additions & 1 deletion src/graph/agentGraph.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { END, MemorySaver, START, StateGraph } from "@langchain/langgraph";
import type { AppConfig } from "../config/defaults.js";
import type { LangSmithConfig } from "../config/observability.js";
import type { AlertService } from "../services/AlertService.js";
import type { ClusteringService } from "../services/ClusteringService.js";
import type { NarrativeLifecycleService } from "../services/NarrativeLifecycleService.js";
Expand Down Expand Up @@ -53,12 +54,28 @@ export const createNarrativeGraph = (dependencies: GraphDependencies) => {
export class NarrativeGraphRunner {
private readonly graph: ReturnType<typeof createNarrativeGraph>;

constructor(dependencies: GraphDependencies) {
constructor(
dependencies: GraphDependencies,
private readonly langSmith: LangSmithConfig = {
tracingEnabled: false,
runName: "narrative-alpha-agent",
tags: []
}
) {
this.graph = createNarrativeGraph(dependencies);
}

async run(input: SystemState, threadId = "default"): Promise<SystemState> {
const result = await this.graph.invoke(input, {
runName: this.langSmith.runName,
tags: this.langSmith.tags,
metadata: {
app: "narrative-alpha-agent",
timestamp: input.timestamp,
documentCount: input.documents.length,
langSmithProject: this.langSmith.project,
tracingEnabled: this.langSmith.tracingEnabled
},
configurable: {
thread_id: threadId
}
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { ReplayEngine, type ReplaySnapshot } from "./backtest/ReplayEngine.js";
export { demoDataset, demoTimeline } from "./backtest/demoDataset.js";
export { createRuntime } from "./agents/createRuntime.js";
export { readNotificationConfigFromEnv } from "./config/notifications.js";
export { readLangSmithConfigFromEnv } from "./config/observability.js";
export { readProviderConfigFromEnv, type AiProvider } from "./config/secrets.js";
export { createNarrativeGraph, NarrativeGraphRunner } from "./graph/agentGraph.js";
export {
Expand All @@ -11,6 +13,7 @@ export {
GeminiLlmClient,
LocalEchoLlmClient
} from "./services/MultiProviderLlmClient.js";
export { DiscordNotifier, type DiscordNotifierOptions } from "./services/AlertService.js";
export { OpenAICompatibleLlmClient } from "./services/OpenAICompatibleLlmClient.js";
export type { Cluster, Document, Narrative, SystemState } from "./types/domain.js";
export type { EmbeddingProvider, LlmClient, NarrativeRepository, Notifier } from "./types/ports.js";
Loading