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
10 changes: 8 additions & 2 deletions apps/server/src/persistence/Layers/SmeConversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,18 @@ const makeSmeConversationRepository = Effect.gen(function* () {
execute: (row) =>
sql`
INSERT INTO sme_conversations (
conversation_id, project_id, title, model,
conversation_id, project_id, title, provider, auth_method, model,
created_at, updated_at, deleted_at
)
VALUES (
${row.conversationId}, ${row.projectId}, ${row.title}, ${row.model},
${row.conversationId}, ${row.projectId}, ${row.title}, ${row.provider}, ${row.authMethod}, ${row.model},
${row.createdAt}, ${row.updatedAt}, ${row.deletedAt}
)
ON CONFLICT (conversation_id)
DO UPDATE SET
title = excluded.title,
provider = excluded.provider,
auth_method = excluded.auth_method,
model = excluded.model,
updated_at = excluded.updated_at,
deleted_at = excluded.deleted_at
Expand All @@ -53,6 +55,8 @@ const makeSmeConversationRepository = Effect.gen(function* () {
conversation_id AS "conversationId",
project_id AS "projectId",
title,
provider,
auth_method AS "authMethod",
model,
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand All @@ -71,6 +75,8 @@ const makeSmeConversationRepository = Effect.gen(function* () {
conversation_id AS "conversationId",
project_id AS "projectId",
title,
provider,
auth_method AS "authMethod",
model,
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/persistence/Migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import Migration0016 from "./Migrations/016_ProjectionThreadsInteractionModeChat
import Migration0017 from "./Migrations/017_EnvironmentVariables.ts";
import Migration0018 from "./Migrations/018_ProjectionThreadsGithubRef.ts";
import Migration0019 from "./Migrations/019_SmeKnowledgeBase.ts";
import Migration0020 from "./Migrations/020_SmeConversationProviderAuth.ts";
import { Effect } from "effect";

/**
Expand Down Expand Up @@ -63,6 +64,7 @@ const loader = Migrator.fromRecord({
"17_EnvironmentVariables": Migration0017,
"18_ProjectionThreadsGithubRef": Migration0018,
"19_SmeKnowledgeBase": Migration0019,
"20_SmeConversationProviderAuth": Migration0020,
});

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as Effect from "effect/Effect";
import * as SqlClient from "effect/unstable/sql/SqlClient";

export default Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;

yield* sql`
ALTER TABLE sme_conversations
ADD COLUMN provider TEXT NOT NULL DEFAULT 'claudeAgent'
`.pipe(Effect.catch(() => Effect.void));

yield* sql`
ALTER TABLE sme_conversations
ADD COLUMN auth_method TEXT NOT NULL DEFAULT 'auto'
`.pipe(Effect.catch(() => Effect.void));

yield* sql`
UPDATE sme_conversations
SET provider = CASE
WHEN lower(model) LIKE 'claude-%' THEN 'claudeAgent'
WHEN lower(model) LIKE 'gpt-%' THEN 'codex'
WHEN lower(model) LIKE 'openclaw/%' OR lower(model) = 'default' THEN 'openclaw'
ELSE 'claudeAgent'
END
WHERE provider IS NULL
OR provider = ''
OR provider = 'claudeAgent'
`;

yield* sql`
UPDATE sme_conversations
SET auth_method = 'auto'
WHERE auth_method IS NULL OR auth_method = ''
`;
});
10 changes: 9 additions & 1 deletion apps/server/src/persistence/Services/SmeConversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
*
* @module SmeConversationRepository
*/
import { IsoDateTime, ProjectId, SmeConversationId } from "@okcode/contracts";
import {
IsoDateTime,
ProjectId,
ProviderKind,
SmeAuthMethod,
SmeConversationId,
} from "@okcode/contracts";
import { Option, Schema, ServiceMap } from "effect";
import type { Effect } from "effect";

Expand All @@ -15,6 +21,8 @@ export const SmeConversationRow = Schema.Struct({
conversationId: SmeConversationId,
projectId: ProjectId,
title: Schema.String,
provider: ProviderKind,
authMethod: SmeAuthMethod,
model: Schema.String,
createdAt: IsoDateTime,
updatedAt: IsoDateTime,
Expand Down
39 changes: 34 additions & 5 deletions apps/server/src/sme/Layers/SmeChatServiceLive.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ProjectId, SmeConversationId, type EnvironmentVariableEntry } from "@okcode/contracts";
import { Effect, Layer, Option } from "effect";
import { Effect, Layer, Option, Stream } from "effect";
import { afterEach, describe, expect, it, vi } from "vitest";

import {
Expand All @@ -21,6 +21,10 @@ import {
type SmeMessageRepositoryShape,
type SmeMessageRow,
} from "../../persistence/Services/SmeMessages.ts";
import {
ProviderService,
type ProviderServiceShape,
} from "../../provider/Services/ProviderService.ts";
import { SmeChatService } from "../Services/SmeChatService.ts";
import { makeSmeChatServiceLive } from "./SmeChatServiceLive.ts";

Expand Down Expand Up @@ -154,6 +158,21 @@ function makeMessageRepository() {
return { repository, rowsByConversation };
}

function makeProviderService(): ProviderServiceShape {
return {
startSession: () => Effect.die("unexpected provider startSession"),
sendTurn: () => Effect.die("unexpected provider sendTurn"),
interruptTurn: () => Effect.void,
respondToRequest: () => Effect.void,
respondToUserInput: () => Effect.void,
stopSession: () => Effect.void,
listSessions: () => Effect.succeed([]),
getCapabilities: () => Effect.die("unexpected provider getCapabilities"),
rollbackConversation: () => Effect.void,
streamEvents: Stream.empty,
};
}

describe("SmeChatServiceLive", () => {
it("uses persisted Anthropic credentials for a successful send and stores the final reply", async () => {
setAnthropicEnv({
Expand All @@ -168,6 +187,8 @@ describe("SmeChatServiceLive", () => {
conversationId,
projectId,
title: "Architecture Q&A",
provider: "claudeAgent",
authMethod: "apiKey",
model: "claude-sonnet-4-6",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
Expand Down Expand Up @@ -209,6 +230,7 @@ describe("SmeChatServiceLive", () => {
Layer.succeed(SmeConversationRepository, makeConversationRepository([conversationRow])),
),
Layer.provideMerge(Layer.succeed(SmeMessageRepository, messageRepo)),
Layer.provideMerge(Layer.succeed(ProviderService, makeProviderService())),
);

const events: Array<unknown> = [];
Expand Down Expand Up @@ -287,6 +309,8 @@ describe("SmeChatServiceLive", () => {
conversationId,
projectId,
title: "Docs sync",
provider: "claudeAgent",
authMethod: "apiKey",
model: "claude-sonnet-4-6",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
Expand All @@ -302,6 +326,7 @@ describe("SmeChatServiceLive", () => {
Layer.succeed(SmeConversationRepository, makeConversationRepository([conversationRow])),
),
Layer.provideMerge(Layer.succeed(SmeMessageRepository, messageRepo)),
Layer.provideMerge(Layer.succeed(ProviderService, makeProviderService())),
);

await expect(
Expand All @@ -314,11 +339,15 @@ describe("SmeChatServiceLive", () => {
});
}).pipe(Effect.provide(layer)),
),
).rejects.toThrow(
"SmeChatError in sendMessage:auth: SME Chat requires ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN.",
);
).rejects.toThrow("SmeChatError in sendMessage:validate: Anthropic API key is missing.");

expect(createClient).not.toHaveBeenCalled();
expect(rowsByConversation.get(conversationId) ?? []).toEqual([]);
expect(rowsByConversation.get(conversationId)).toEqual([
expect.objectContaining({
role: "user",
text: "Can you summarize the docs?",
isStreaming: false,
}),
]);
});
});
Loading
Loading