Skip to content

Commit

Permalink
try Effect.Service (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart authored Oct 7, 2024
1 parent 3abcf52 commit ba54464
Show file tree
Hide file tree
Showing 15 changed files with 587 additions and 627 deletions.
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "",
"type": "module",
"packageManager": "pnpm@9.5.0",
"packageManager": "pnpm@9.12.0",
"exports": {
".": "./dist/index.js",
"./*": "./dist/*.js"
Expand All @@ -18,23 +18,23 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@effect/ai": "https://pkg.pr.new/Effect-TS/effect/@effect/ai@2219858",
"@effect/ai-openai": "https://pkg.pr.new/Effect-TS/effect/@effect/ai-openai@2219858",
"@effect/experimental": "https://pkg.pr.new/Effect-TS/effect/@effect/experimental@2219858",
"@effect/ai": "https://pkg.pr.new/Effect-TS/effect/@effect/ai@4d20867",
"@effect/ai-openai": "https://pkg.pr.new/Effect-TS/effect/@effect/ai-openai@4d20867",
"@effect/experimental": "https://pkg.pr.new/Effect-TS/effect/@effect/experimental@4d20867",
"@effect/language-service": "^0.1.0",
"@effect/opentelemetry": "^0.37.4",
"@effect/platform": "^0.66.2",
"@effect/platform-node": "^0.61.3",
"@effect/schema": "^0.74.1",
"@octokit/types": "^13.5.1",
"@effect/opentelemetry": "^0.38.0",
"@effect/platform": "^0.67.0",
"@effect/platform-node": "^0.62.0",
"@effect/schema": "^0.75.0",
"@octokit/types": "^13.6.1",
"@opentelemetry/exporter-metrics-otlp-http": "^0.53.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.53.0",
"@opentelemetry/sdk-metrics": "^1.26.0",
"@opentelemetry/sdk-trace-base": "^1.26.0",
"@opentelemetry/sdk-trace-node": "^1.26.0",
"@types/node": "^22.7.1",
"dfx": "^0.103.0",
"effect": "^3.8.4",
"@types/node": "^22.7.4",
"dfx": "^0.104.0",
"effect": "^3.9.0",
"html-entities": "^2.5.2",
"octokit": "^4.0.2",
"prettier": "^3.3.3",
Expand Down
260 changes: 130 additions & 130 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

106 changes: 50 additions & 56 deletions src/Ai.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { OpenAiCompletions, OpenAiClient } from "@effect/ai-openai"
import { Chunk, Config, Context, Effect, Layer, pipe } from "effect"
import { AiInput, Completions } from "@effect/ai"
import * as Str from "./utils/String.js"
import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai"
import { NodeHttpClient } from "@effect/platform-node"
import { Chunk, Config, Effect, Layer, pipe } from "effect"
import * as Str from "./utils/String.js"

export const OpenAiLive = OpenAiClient.layerConfig({
apiKey: Config.redacted("OPENAI_API_KEY"),
Expand All @@ -15,66 +15,60 @@ export const CompletionsLive = OpenAiCompletions.layer({
model: "gpt-4o",
}).pipe(Layer.provide(OpenAiLive))

const make = Effect.gen(function* () {
const completions = yield* Completions.Completions
export class AiHelpers extends Effect.Service<AiHelpers>()("app/AiHelpers", {
effect: Effect.gen(function* () {
const completions = yield* Completions.Completions

const generateTitle = (prompt: string) =>
completions.create.pipe(
AiInput.provideSystem(
"Create a short title summarizing the message. Do not include markdown in the title.",
),
AiInput.provide(Str.truncateWords(prompt, 500)),
Effect.provideService(OpenAiCompletions.OpenAiConfig, {
temperature: 0.25,
max_tokens: 64,
}),
Effect.map(_ => cleanTitle(_.text)),
Effect.withSpan("Ai.generateTitle", { attributes: { prompt } }),
)
const generateTitle = (prompt: string) =>
completions.create.pipe(
AiInput.provideSystem(
"Create a short title summarizing the message. Do not include markdown in the title.",
),
AiInput.provide(Str.truncateWords(prompt, 500)),
Effect.provideService(OpenAiCompletions.OpenAiConfig, {
temperature: 0.25,
max_tokens: 64,
}),
Effect.map(_ => cleanTitle(_.text)),
Effect.withSpan("Ai.generateTitle", { attributes: { prompt } }),
)

const generateDocs = (
title: string,
messages: AiInput.AiInput.Type,
instruction = "Create a documentation article from the above chat messages. The article should be written in markdown and should contain code examples where appropiate.",
) =>
completions.create.pipe(
Effect.provideService(
AiInput.SystemInstruction,
`You are a helpful assistant for the Effect-TS ecosystem.
const generateDocs = (
title: string,
messages: AiInput.AiInput.Type,
instruction = "Create a documentation article from the above chat messages. The article should be written in markdown and should contain code examples where appropiate.",
) =>
completions.create.pipe(
Effect.provideService(
AiInput.SystemInstruction,
`You are a helpful assistant for the Effect-TS ecosystem.
The title of this chat is "${title}".`,
),
AiInput.provideEffect(
Chunk.appendAll(messages, AiInput.make(instruction)).pipe(
AiInput.truncate(30_000),
Effect.provideService(Completions.Completions, completions),
),
),
Effect.map(_ => _.text),
)

const generateSummary = (title: string, messages: AiInput.AiInput.Type) =>
generateDocs(
title,
messages,
"Summarize the above messages. Also include some key takeaways.",
)
AiInput.provideEffect(
Chunk.appendAll(messages, AiInput.make(instruction)).pipe(
AiInput.truncate(30_000),
Effect.provideService(Completions.Completions, completions),
),
),
Effect.map(_ => _.text),
)

return {
generateTitle,
generateDocs,
generateSummary,
} as const
})
const generateSummary = (title: string, messages: AiInput.AiInput.Type) =>
generateDocs(
title,
messages,
"Summarize the above messages. Also include some key takeaways.",
)

export class AiHelpers extends Context.Tag("app/AiHelpers")<
AiHelpers,
Effect.Effect.Success<typeof make>
>() {
static Live = Layer.effect(AiHelpers, make).pipe(
Layer.provide(CompletionsLive),
)
}
return {
generateTitle,
generateDocs,
generateSummary,
} as const
}),
dependencies: [CompletionsLive],
}) {}

const cleanTitle = (_: string) =>
pipe(Str.firstParagraph(_), Str.removeQuotes, Str.removePeriod)
4 changes: 2 additions & 2 deletions src/AutoThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ const make = Effect.gen(function* () {
)

export const AutoThreadsLive = Layer.scopedDiscard(make).pipe(
Layer.provide(ChannelsCache.Live),
Layer.provide(AiHelpers.Live),
Layer.provide(ChannelsCache.Default),
Layer.provide(AiHelpers.Default),
Layer.provide(DiscordLive),
)
31 changes: 14 additions & 17 deletions src/ChannelsCache.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { DiscordLive } from "bot/Discord"
import { Cache } from "dfx"
import { CachePrelude } from "dfx/gateway"
import { Context, Duration, Effect, Layer } from "effect"
import { Duration, Effect } from "effect"
import { DiscordLive } from "./Discord.js"

const makeChannelsCache = CachePrelude.channels(
Cache.memoryTTLParentDriver({
ttl: Duration.minutes(30),
strategy: "activity",
}),
)

export class ChannelsCache extends Context.Tag("app/ChannelsCache")<
ChannelsCache,
Effect.Effect.Success<typeof makeChannelsCache>
>() {
static Live = Layer.scoped(this, makeChannelsCache).pipe(
Layer.provide(DiscordLive),
)
}
export class ChannelsCache extends Effect.Service<ChannelsCache>()(
"app/ChannelsCache",
{
scoped: CachePrelude.channels(
Cache.memoryTTLParentDriver({
ttl: Duration.minutes(30),
strategy: "activity",
}),
),
dependencies: [DiscordLive],
},
) {}
2 changes: 1 addition & 1 deletion src/DadJokes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
HttpClientResponse,
} from "@effect/platform"
import { Schema } from "@effect/schema"
import { InteractionsRegistry } from "dfx/gateway"
import { Ix } from "dfx"
import { InteractionsRegistry } from "dfx/gateway"
import { Effect, flow, Layer, Schedule } from "effect"
import { DiscordLive } from "./Discord.js"

Expand Down
2 changes: 1 addition & 1 deletion src/Discord.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NodeHttpClient, NodeSocket } from "@effect/platform-node"
import { DiscordIxLive } from "dfx/gateway"
import { DiscordConfig, Intents } from "dfx"
import { DiscordIxLive } from "dfx/gateway"
import { Config, Layer } from "effect"

export const DiscordLive = DiscordIxLive.pipe(
Expand Down
87 changes: 40 additions & 47 deletions src/Github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { nestedConfigProvider } from "bot/utils/Config"
import {
Chunk,
Config,
Context,
Data,
Effect,
Layer,
Option,
Redacted,
Stream,
Expand All @@ -18,60 +16,55 @@ export class GithubError extends Data.TaggedError("GithubError")<{
readonly reason: unknown
}> {}

const make = Effect.gen(function* () {
const token = yield* Config.redacted("token")
const octokit = new Octokit({ auth: Redacted.value(token) })
export class Github extends Effect.Service<Github>()("app/Github", {
effect: Effect.gen(function* () {
const token = yield* Config.redacted("token")
const octokit = new Octokit({ auth: Redacted.value(token) })

const rest = octokit.rest
type Endpoints = typeof rest
const rest = octokit.rest
type Endpoints = typeof rest

const request = <A>(f: (_: Endpoints) => Promise<A>) =>
Effect.withSpan(
Effect.tryPromise({
try: () => f(rest),
catch: reason => new GithubError({ reason }),
}),
"Github.request",
)

const wrap =
<A, Args extends any[]>(
f: (_: Endpoints) => (...args: Args) => Promise<OctokitResponse<A>>,
) =>
(...args: Args) =>
Effect.map(
const request = <A>(f: (_: Endpoints) => Promise<A>) =>
Effect.withSpan(
Effect.tryPromise({
try: () => f(rest)(...args),
try: () => f(rest),
catch: reason => new GithubError({ reason }),
}),
_ => _.data,
"Github.request",
)

const stream = <A>(
f: (_: Endpoints, page: number) => Promise<OctokitResponse<A[]>>,
) =>
Stream.paginateChunkEffect(0, page =>
Effect.map(
Effect.tryPromise({
try: () => f(rest, page),
catch: reason => new GithubError({ reason }),
}),
_ => [
Chunk.unsafeFromArray(_.data),
maybeNextPage(page, _.headers.link),
],
),
)
const wrap =
<A, Args extends any[]>(
f: (_: Endpoints) => (...args: Args) => Promise<OctokitResponse<A>>,
) =>
(...args: Args) =>
Effect.map(
Effect.tryPromise({
try: () => f(rest)(...args),
catch: reason => new GithubError({ reason }),
}),
_ => _.data,
)

return { octokit, token, request, wrap, stream } as const
}).pipe(Effect.withConfigProvider(nestedConfigProvider("github")))
const stream = <A>(
f: (_: Endpoints, page: number) => Promise<OctokitResponse<A[]>>,
) =>
Stream.paginateChunkEffect(0, page =>
Effect.map(
Effect.tryPromise({
try: () => f(rest, page),
catch: reason => new GithubError({ reason }),
}),
_ => [
Chunk.unsafeFromArray(_.data),
maybeNextPage(page, _.headers.link),
],
),
)

export class Github extends Context.Tag("app/Github")<
Github,
Effect.Effect.Success<typeof make>
>() {
static Live = Layer.effect(Github, make)
}
return { octokit, token, request, wrap, stream } as const
}).pipe(Effect.withConfigProvider(nestedConfigProvider("github"))),
}) {}

// == helpers

Expand Down
10 changes: 5 additions & 5 deletions src/Issueifier.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AiInput, AiRole } from "@effect/ai"
import { ChannelsCache } from "bot/ChannelsCache"
import { DiscordLive } from "bot/Discord"
import { Github } from "bot/Github"
Expand All @@ -16,7 +17,6 @@ import {
pipe,
} from "effect"
import { AiHelpers } from "./Ai.js"
import { AiInput, AiRole } from "@effect/ai"

export class NotInThreadError extends Data.TaggedError(
"NotInThreadError",
Expand Down Expand Up @@ -173,8 +173,8 @@ https://discord.com/channels/${channel.guild_id}/${channel.id}

export const IssueifierLive = Layer.scopedDiscard(make).pipe(
Layer.provide(DiscordLive),
Layer.provide(ChannelsCache.Live),
Layer.provide(Messages.Live),
Layer.provide(AiHelpers.Live),
Layer.provide(Github.Live),
Layer.provide(ChannelsCache.Default),
Layer.provide(Messages.Default),
Layer.provide(AiHelpers.Default),
Layer.provide(Github.Default),
)
Loading

0 comments on commit ba54464

Please sign in to comment.