mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-06-30 17:09:38 +02:00
Migrate Claude provider to Effect AI
This commit is contained in:
parent
24a2447cc3
commit
1d45307387
5 changed files with 116 additions and 156 deletions
|
|
@ -17,7 +17,7 @@ adapter and native request/response PubSub slices:
|
|||
|
||||
| Signal | Count |
|
||||
| --- | ---: |
|
||||
| `Effect.runPromise` | 170 |
|
||||
| `Effect.runPromise` | 169 |
|
||||
| `Effect.runPromiseWith` | 0 |
|
||||
| `Effect.cached` | 0 |
|
||||
| `Layer.succeed` | 12 |
|
||||
|
|
@ -150,6 +150,10 @@ Notes:
|
|||
`{ id, value }` envelopes through native `effect/PubSub`, and each request
|
||||
uses a scoped `PubSub.Subscription` plus `Stream.fromSubscription` to wait
|
||||
for its matching response.
|
||||
- The Claude Effect AI slice moved the Claude provider off the direct
|
||||
`@anthropic-ai/sdk` wrapper and onto `@effect/ai-anthropic`
|
||||
`AnthropicLanguageModel` through `makeLanguageModelProvider`. The direct SDK
|
||||
dependency was removed from `@trustgraph/flow`.
|
||||
- A focused broker-backend scout found no remaining P0 broker runtime rewrite
|
||||
after the producer, NATS, consumer concurrency, rate-limit, and
|
||||
request-response stop slices. `PubSubBackend` remains an intentional
|
||||
|
|
@ -1402,6 +1406,29 @@ Notes:
|
|||
- `cd ts && bun run check:tsgo`
|
||||
- `cd ts/packages/base && bunx --bun vitest run src/__tests__/messaging-runtime.test.ts src/__tests__/request-response.test.ts src/__tests__/flow-spec-runtime.test.ts`
|
||||
|
||||
### 2026-06-02: Claude Effect AI Provider Slice
|
||||
|
||||
- Status: migrated and package-verified.
|
||||
- Completed:
|
||||
- Replaced the direct `@anthropic-ai/sdk` provider implementation with
|
||||
`@effect/ai-anthropic` `AnthropicLanguageModel` plus the shared
|
||||
`makeLanguageModelProvider` adapter.
|
||||
- Preserved `CLAUDE_KEY`, default model, temperature, max output, processor,
|
||||
and public `LlmProvider` compatibility behavior.
|
||||
- Added Claude provider config coverage for `CLAUDE_KEY` env fallback and the
|
||||
missing-key tagged config error.
|
||||
- Removed the direct `@anthropic-ai/sdk` dependency and its lockfile entries
|
||||
from `@trustgraph/flow`.
|
||||
- Verification:
|
||||
- `cd ts && bun run check:tsgo`
|
||||
- `cd ts/packages/flow && bunx --bun vitest run src/__tests__/text-completion-providers.test.ts src/__tests__/text-completion-common.test.ts`
|
||||
- `cd ts/packages/flow && bun run build`
|
||||
- `cd ts && bun run check`
|
||||
- `cd ts && bun run build`
|
||||
- `cd ts && bun run test`
|
||||
- `cd ts && bun run lint`
|
||||
- `git diff --check`
|
||||
|
||||
## Subagent Findings To Preserve
|
||||
|
||||
- MCP/workbench:
|
||||
|
|
@ -1487,10 +1514,9 @@ Notes:
|
|||
local provider layer-construction cleanup is complete; remaining provider
|
||||
work is adapter/parity work, not `Layer.succeed` cleanup.
|
||||
- The `effect/unstable/ai/LanguageModel` to TrustGraph `LlmProvider` adapter
|
||||
baseline is complete. The next provider PR should migrate Claude through
|
||||
that adapter with provider-specific parity tests. Direct OpenAI, Azure, and
|
||||
OpenAI-compatible swaps are no-ops until Responses-vs-Chat-Completions
|
||||
parity is proven.
|
||||
baseline is complete, and Claude now uses `@effect/ai-anthropic` through
|
||||
that adapter. Direct OpenAI, Azure, and OpenAI-compatible swaps are no-ops
|
||||
until Responses-vs-Chat-Completions parity is proven.
|
||||
- FalkorDB scoped lifecycle is complete for triples query/store. Use the
|
||||
fakeable client/graph factory pattern from that slice for future storage
|
||||
client tests.
|
||||
|
|
@ -1549,7 +1575,7 @@ Notes:
|
|||
- Treat the legacy consumer facade as a completed compatibility wrapper over
|
||||
`makeEffectConsumerFromPubSub`; do not flag blocking `start()` semantics.
|
||||
|
||||
### P2: Effect AI Provider Adapter Cleanup
|
||||
### No-op: Remaining Effect AI Provider Swaps
|
||||
|
||||
- TrustGraph evidence:
|
||||
- `ts/packages/flow/src/model/text-completion/*.ts`
|
||||
|
|
@ -1559,10 +1585,12 @@ Notes:
|
|||
- Rewrite shape:
|
||||
- Adapter baseline is complete: `makeLanguageModelProvider` bridges
|
||||
`LanguageModel` into `LlmProvider`.
|
||||
- Claude is the first plausible provider migration through the adapter.
|
||||
- Claude migration is complete through `@effect/ai-anthropic`.
|
||||
- Do not directly swap OpenAI, Azure, or OpenAI-compatible providers yet:
|
||||
current TrustGraph code uses Chat Completions/local-server semantics while
|
||||
`@effect/ai-openai` is Responses API backed.
|
||||
- Do not directly swap Mistral or Ollama until an installed Effect provider
|
||||
package exists or a parity-backed local Effect wrapper is designed.
|
||||
- Tests:
|
||||
- Provider parity for `LlmResult`, final streaming chunk token counts, 429
|
||||
mapping, missing-token config failures, and OpenAI-compatible local-server
|
||||
|
|
@ -1603,8 +1631,7 @@ Notes:
|
|||
|
||||
## Recommended PR Order
|
||||
|
||||
1. Claude provider parity through the Effect AI `LanguageModel` adapter.
|
||||
2. MCP Effect stdio parity and canonicalization.
|
||||
1. MCP Effect stdio parity and canonicalization.
|
||||
|
||||
## No-Op Rules
|
||||
|
||||
|
|
|
|||
|
|
@ -97,7 +97,6 @@
|
|||
"name": "@trustgraph/flow",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@effect/ai-anthropic": "4.0.0-beta.75",
|
||||
"@effect/ai-openai": "4.0.0-beta.75",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.75",
|
||||
|
|
@ -200,8 +199,6 @@
|
|||
},
|
||||
},
|
||||
"packages": {
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.39.0", "", { "dependencies": { "@types/node": "18.19.130", "@types/node-fetch": "2.6.13", "abort-controller": "3.0.0", "agentkeepalive": "4.6.0", "form-data-encoder": "1.7.2", "formdata-node": "4.4.1", "node-fetch": "2.7.0" } }, "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "7.28.5", "js-tokens": "4.0.0", "picocolors": "1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
|
||||
|
|
@ -1350,8 +1347,6 @@
|
|||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "5.26.5" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
|
@ -1394,8 +1389,6 @@
|
|||
|
||||
"vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
|
||||
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
"@trustgraph/base/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"@trustgraph/client/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
|
|
|||
|
|
@ -11,16 +11,6 @@
|
|||
"test": "bunx --bun vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"@fastify/websocket": "^11.0.0",
|
||||
"@qdrant/js-client-rest": "^1.13.0",
|
||||
"@trustgraph/base": "workspace:*",
|
||||
"falkordb": "^5.0.0",
|
||||
"fastify": "^5.2.0",
|
||||
"ollama": "^0.6.3",
|
||||
"@mistralai/mistralai": "^1.0.0",
|
||||
"@effect/platform-node": "4.0.0-beta.75",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.75",
|
||||
"@effect/ai-anthropic": "4.0.0-beta.75",
|
||||
"@effect/ai-openai": "4.0.0-beta.75",
|
||||
"@effect/ai-openrouter": "4.0.0-beta.75",
|
||||
|
|
@ -29,10 +19,19 @@
|
|||
"@effect/opentelemetry": "4.0.0-beta.75",
|
||||
"@effect/platform-browser": "4.0.0-beta.75",
|
||||
"@effect/platform-bun": "4.0.0-beta.75",
|
||||
"@effect/platform-node": "4.0.0-beta.75",
|
||||
"@effect/platform-node-shared": "4.0.0-beta.75",
|
||||
"@effect/tsgo": "0.13.0",
|
||||
"@effect/vitest": "4.0.0-beta.75",
|
||||
"@fastify/websocket": "^11.0.0",
|
||||
"@mistralai/mistralai": "^1.0.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"@qdrant/js-client-rest": "^1.13.0",
|
||||
"@trustgraph/base": "workspace:*",
|
||||
"effect": "4.0.0-beta.75",
|
||||
"falkordb": "^5.0.0",
|
||||
"fastify": "^5.2.0",
|
||||
"ollama": "^0.6.3",
|
||||
"openai": "^4.85.0",
|
||||
"pdfjs-dist": "^5.6.205"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -83,4 +83,35 @@ describe("text completion provider construction", () => {
|
|||
});
|
||||
}),
|
||||
);
|
||||
|
||||
it.effect(
|
||||
"loads Claude API key from config provider",
|
||||
Effect.fnUntraced(function* () {
|
||||
const provider = yield* makeClaudeProviderEffect({ id: "claude" }).pipe(
|
||||
Effect.provide(
|
||||
ConfigProvider.layer(
|
||||
ConfigProvider.fromEnv({ env: { CLAUDE_KEY: "env-key" } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(provider.supportsStreaming()).toBe(true);
|
||||
}),
|
||||
);
|
||||
|
||||
it.effect(
|
||||
"fails missing Claude API key as a tagged config error",
|
||||
Effect.fnUntraced(function* () {
|
||||
const error = yield* makeClaudeProviderEffect({ id: "claude" }).pipe(
|
||||
Effect.flip,
|
||||
Effect.provide(emptyConfig),
|
||||
);
|
||||
|
||||
expect(error).toMatchObject({
|
||||
_tag: "TextCompletionConfigError",
|
||||
provider: "Claude",
|
||||
key: "CLAUDE_KEY",
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Python reference: trustgraph-flow/trustgraph/model/text_completion/claude/llm.py
|
||||
*/
|
||||
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic";
|
||||
import { NodeRuntime } from "@effect/platform-node";
|
||||
import {
|
||||
makeLlmService,
|
||||
|
|
@ -13,18 +13,14 @@ import {
|
|||
type Llm,
|
||||
type LlmProvider,
|
||||
type ProcessorConfig,
|
||||
type LlmResult,
|
||||
type LlmChunk,
|
||||
} from "@trustgraph/base";
|
||||
import { Effect, Layer, ManagedRuntime, Stream } from "effect";
|
||||
import { Effect, Layer, ManagedRuntime, Redacted } from "effect";
|
||||
import { FetchHttpClient } from "effect/unstable/http";
|
||||
import {
|
||||
llmStreamPart,
|
||||
makeLanguageModelProvider,
|
||||
makeTextCompletionLayer,
|
||||
optionalStringConfig,
|
||||
providerStatusError,
|
||||
requiredString,
|
||||
streamTextCompletionChunks,
|
||||
toAsyncGenerator,
|
||||
type TextCompletionConfigError,
|
||||
type TextCompletionRuntimeError,
|
||||
} from "./common.ts";
|
||||
|
|
@ -43,141 +39,55 @@ type ResolvedClaudeConfig = {
|
|||
readonly apiKey: string;
|
||||
};
|
||||
|
||||
const loadClaudeConfig = Effect.fn("loadClaudeConfig")(function*(config: ClaudeProcessorConfig) {
|
||||
const apiKey = yield* requiredString(
|
||||
config.apiKey ?? (yield* optionalStringConfig("Claude", "CLAUDE_KEY")),
|
||||
"Claude",
|
||||
"CLAUDE_KEY",
|
||||
"Claude API key not specified",
|
||||
);
|
||||
|
||||
return {
|
||||
defaultModel: config.model ?? "claude-sonnet-4-20250514",
|
||||
defaultTemperature: config.temperature ?? 0.0,
|
||||
maxOutput: config.maxOutput ?? 8192,
|
||||
apiKey,
|
||||
} satisfies ResolvedClaudeConfig;
|
||||
});
|
||||
|
||||
const mapClaudeError = (error: unknown): TextCompletionRuntimeError =>
|
||||
providerStatusError("Claude", error);
|
||||
|
||||
const makeClaudeProviderFromClient = (
|
||||
resolved: ResolvedClaudeConfig,
|
||||
client: Anthropic,
|
||||
): LlmProvider => {
|
||||
const {
|
||||
defaultModel,
|
||||
defaultTemperature,
|
||||
maxOutput,
|
||||
} = resolved;
|
||||
const loadClaudeConfig = Effect.fn("loadClaudeConfig")(function* (config: ClaudeProcessorConfig) {
|
||||
const apiKey = yield* requiredString(
|
||||
config.apiKey ?? (yield* optionalStringConfig("Claude", "CLAUDE_KEY")),
|
||||
"Claude",
|
||||
"CLAUDE_KEY",
|
||||
"Claude API key not specified",
|
||||
);
|
||||
|
||||
return {
|
||||
generateContent: (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): Promise<LlmResult> => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
defaultModel: config.model ?? "claude-sonnet-4-20250514",
|
||||
defaultTemperature: config.temperature ?? 0.0,
|
||||
maxOutput: config.maxOutput ?? 8192,
|
||||
apiKey,
|
||||
} satisfies ResolvedClaudeConfig;
|
||||
});
|
||||
|
||||
return Effect.runPromise(
|
||||
Effect.tryPromise({
|
||||
try: () =>
|
||||
client.messages.create({
|
||||
model: modelName,
|
||||
max_tokens: maxOutput,
|
||||
temperature: temp,
|
||||
system,
|
||||
messages: [
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
}),
|
||||
catch: mapClaudeError,
|
||||
}).pipe(
|
||||
Effect.map((response): LlmResult => {
|
||||
const firstContent = response.content[0];
|
||||
const text = firstContent?.type === "text"
|
||||
? firstContent.text
|
||||
: "";
|
||||
|
||||
return {
|
||||
text,
|
||||
inToken: response.usage.input_tokens,
|
||||
outToken: response.usage.output_tokens,
|
||||
model: modelName,
|
||||
};
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
supportsStreaming: () => true,
|
||||
generateContentStream: (
|
||||
system: string,
|
||||
prompt: string,
|
||||
model?: string,
|
||||
temperature?: number,
|
||||
): AsyncGenerator<LlmChunk> => {
|
||||
const modelName = model ?? defaultModel;
|
||||
const temp = temperature ?? defaultTemperature;
|
||||
|
||||
const stream = Stream.fromEffect(
|
||||
Effect.try({
|
||||
try: () =>
|
||||
client.messages.stream({
|
||||
model: modelName,
|
||||
max_tokens: maxOutput,
|
||||
temperature: temp,
|
||||
system,
|
||||
messages: [
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
}),
|
||||
catch: mapClaudeError,
|
||||
}),
|
||||
).pipe(
|
||||
Stream.flatMap((anthropicStream) =>
|
||||
streamTextCompletionChunks(anthropicStream, {
|
||||
model: modelName,
|
||||
mapError: mapClaudeError,
|
||||
extract: (event) =>
|
||||
event.type === "content_block_delta" && event.delta.type === "text_delta"
|
||||
? llmStreamPart({ text: event.delta.text })
|
||||
: llmStreamPart({}),
|
||||
finalTokens: Effect.tryPromise({
|
||||
try: () => anthropicStream.finalMessage(),
|
||||
catch: mapClaudeError,
|
||||
}).pipe(
|
||||
Effect.map((finalMessage) => ({
|
||||
inToken: finalMessage.usage.input_tokens,
|
||||
outToken: finalMessage.usage.output_tokens,
|
||||
})),
|
||||
),
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
return toAsyncGenerator(Stream.toAsyncIterable(stream), mapClaudeError);
|
||||
},
|
||||
} satisfies LlmProvider;
|
||||
};
|
||||
const makeClaudeRuntime = (apiKey: string) =>
|
||||
ManagedRuntime.make(
|
||||
AnthropicClient.layer({
|
||||
apiKey: Redacted.make(apiKey),
|
||||
}).pipe(
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
),
|
||||
);
|
||||
|
||||
export function makeClaudeProvider(config: ClaudeProcessorConfig): LlmProvider {
|
||||
return Effect.runSync(makeClaudeProviderEffect(config));
|
||||
}
|
||||
|
||||
export const makeClaudeProviderEffect = Effect.fn("makeClaudeProvider")(function*(
|
||||
export const makeClaudeProviderEffect = Effect.fn("makeClaudeProvider")(function* (
|
||||
config: ClaudeProcessorConfig,
|
||||
) {
|
||||
const resolved = yield* loadClaudeConfig(config);
|
||||
const client = yield* Effect.try({
|
||||
try: () => new Anthropic({ apiKey: resolved.apiKey }),
|
||||
catch: mapClaudeError,
|
||||
});
|
||||
|
||||
yield* Effect.log("[Claude] LLM service initialized");
|
||||
return makeClaudeProviderFromClient(resolved, client);
|
||||
return makeLanguageModelProvider({
|
||||
provider: "Claude",
|
||||
defaultModel: resolved.defaultModel,
|
||||
defaultTemperature: resolved.defaultTemperature,
|
||||
runtime: makeClaudeRuntime(resolved.apiKey),
|
||||
makeLanguageModel: ({ model, temperature }) =>
|
||||
AnthropicLanguageModel.make({
|
||||
model,
|
||||
config: {
|
||||
max_tokens: resolved.maxOutput,
|
||||
temperature,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
export type ClaudeProcessor = ReturnType<typeof makeClaudeProcessor>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue