├── .envrc ├── .npmrc ├── public ├── favicon.png ├── images │ ├── qr.png │ ├── avatar.png │ ├── uptime.png │ ├── dad-joke.webp │ ├── use-effect.jpeg │ ├── augmented-llm.webp │ └── effect-bot-solving-ai-chaos.webp └── effect-logo.png ├── global-top.vue ├── .gitignore ├── vercel.json ├── setup └── shiki.ts ├── tsconfig.json ├── test └── dummy.test.ts ├── netlify.toml ├── tsconfig.src.json ├── README.md ├── tsconfig.test.json ├── global-bottom.vue ├── vitest.config.ts ├── src ├── examples │ ├── 005_provider-specific-configuration-01.ts │ ├── 001_using-effect-ai-01.ts │ ├── 004_planning-interactions-01.ts │ ├── 005_provider-specific-configuration-02.ts │ ├── 002_using-provider-integrations-01.ts │ ├── 004_planning-interactions-02.ts │ ├── 002_using-provider-integrations-02.ts │ ├── 002_using-provider-integrations-03.ts │ ├── 003_creating-provider-clients-01.ts │ ├── 003_creating-provider-clients-02.ts │ ├── 005_provider-specific-configuration-03.ts │ ├── 005_provider-specific-configuration-04.ts │ ├── 002_using-provider-integrations-04.ts │ └── 004_planning-interactions-03.ts ├── workflows │ ├── 003_parallel.ts │ ├── 004_orchestrator_workers.ts │ ├── 001_sequential.ts │ ├── 002_routing.ts │ └── 005_evaluator_optimizer.ts └── demos │ └── ai-toolkit.ts ├── flake.nix ├── .vscode └── settings.json ├── flake.lock ├── tsconfig.base.json ├── layouts └── about-me.vue ├── package.json ├── eslint.config.js └── slides.md /.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # for pnpm 2 | shamefully-hoist=true 3 | auto-install-peers=true 4 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IMax153/effect-days-2025-keynote/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/images/qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IMax153/effect-days-2025-keynote/HEAD/public/images/qr.png -------------------------------------------------------------------------------- /public/effect-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IMax153/effect-days-2025-keynote/HEAD/public/effect-logo.png -------------------------------------------------------------------------------- /public/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IMax153/effect-days-2025-keynote/HEAD/public/images/avatar.png -------------------------------------------------------------------------------- /public/images/uptime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IMax153/effect-days-2025-keynote/HEAD/public/images/uptime.png -------------------------------------------------------------------------------- /public/images/dad-joke.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IMax153/effect-days-2025-keynote/HEAD/public/images/dad-joke.webp -------------------------------------------------------------------------------- /public/images/use-effect.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IMax153/effect-days-2025-keynote/HEAD/public/images/use-effect.jpeg -------------------------------------------------------------------------------- /public/images/augmented-llm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IMax153/effect-days-2025-keynote/HEAD/public/images/augmented-llm.webp -------------------------------------------------------------------------------- /global-top.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /public/images/effect-bot-solving-ai-chaos.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IMax153/effect-days-2025-keynote/HEAD/public/images/effect-bot-solving-ai-chaos.webp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local 5 | .direnv 6 | .vite-inspect 7 | .remote-assets 8 | components.d.ts 9 | .env 10 | scratchpad 11 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/index.html" 6 | } 7 | ], 8 | "buildCommand": "pnpm run build", 9 | "outputDirectory": "dist" 10 | } 11 | -------------------------------------------------------------------------------- /setup/shiki.ts: -------------------------------------------------------------------------------- 1 | /* ./setup/shiki.ts */ 2 | import { defineShikiSetup } from "@slidev/types" 3 | 4 | export default defineShikiSetup(() => { 5 | return { 6 | themes: { 7 | dark: "dracula-soft" 8 | } 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "composite": true 5 | }, 6 | "include": [], 7 | "references": [ 8 | { "path": "./tsconfig.src.json" }, 9 | { "path": "./tsconfig.test.json" } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/dummy.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "@effect/vitest" 2 | import { Effect } from "effect" 3 | 4 | describe("Dummy", () => { 5 | it.effect( 6 | "should work", 7 | Effect.fnUntraced(function*() { 8 | expect.anything() 9 | }) 10 | ) 11 | }) 12 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "pnpm run build" 4 | 5 | [build.environment] 6 | NODE_VERSION = "20" 7 | 8 | [[redirects]] 9 | from = "/.well-known/*" 10 | to = "/.well-known/:splat" 11 | status = 200 12 | 13 | [[redirects]] 14 | from = "/*" 15 | to = "/index.html" 16 | status = 200 17 | -------------------------------------------------------------------------------- /tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src/**/*"], 4 | "compilerOptions": { 5 | "types": ["node"], 6 | "outDir": "build/esm", 7 | "declarationDir": "build/dts", 8 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 9 | "rootDir": "src" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to [Slidev](https://github.com/slidevjs/slidev)! 2 | 3 | To start the slide show: 4 | 5 | - `pnpm install` 6 | - `pnpm dev` 7 | - visit 8 | 9 | Edit the [slides.md](./slides.md) to see the changes. 10 | 11 | Learn more about Slidev at the [documentation](https://sli.dev/). 12 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["test"], 4 | "compilerOptions": { 5 | "types": ["node"], 6 | "outDir": "build/test", 7 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 8 | "rootDir": "test" 9 | }, 10 | "references": [{ "path": "./tsconfig.src.json" }] 11 | } 12 | -------------------------------------------------------------------------------- /global-bottom.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | esbuild: { 5 | target: "es2020" 6 | }, 7 | test: { 8 | fakeTimers: { 9 | toFake: undefined 10 | }, 11 | sequence: { 12 | concurrent: true 13 | }, 14 | include: ["test/**/*.test.ts"] 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/examples/005_provider-specific-configuration-01.ts: -------------------------------------------------------------------------------- 1 | import { Completions } from "@effect/ai" 2 | import { Console, Effect } from "effect" 3 | 4 | // ┌─── Effect 5 | // ▼ 6 | const getDadJoke = Effect.gen(function*() { 7 | const completions = yield* Completions.Completions 8 | return yield* completions.create("Generate a hilarious dad joke").pipe( 9 | Effect.andThen((response) => Console.log(response.text)) 10 | ) 11 | }) 12 | -------------------------------------------------------------------------------- /src/examples/001_using-effect-ai-01.ts: -------------------------------------------------------------------------------- 1 | import { Completions } from "@effect/ai" 2 | import { Console, Effect } from "effect" 3 | 4 | // Using the `Completions` service will add it to your program's requirements 5 | // 6 | // ┌─── Effect 7 | // ▼ 8 | const getDadJoke = Effect.gen(function*() { 9 | const completions = yield* Completions.Completions 10 | return yield* completions.create("Generate a hilarious dad joke").pipe( 11 | Effect.andThen((response) => Console.log(response.text)) 12 | ) 13 | }) 14 | -------------------------------------------------------------------------------- /src/examples/004_planning-interactions-01.ts: -------------------------------------------------------------------------------- 1 | import type { Completions } from "@effect/ai" 2 | import { OpenAiCompletions } from "@effect/ai-openai" 3 | import { Effect } from "effect" 4 | 5 | type GetDadJokeError = NetworkError | ProviderOutage 6 | 7 | declare const getDadJoke: Effect.Effect 8 | 9 | // ┌─── Effect 10 | // ▼ 11 | const main = Effect.gen(function*() { 12 | const gpt4o = yield* OpenAiCompletions.model("gpt-4o") 13 | yield* gpt4o.provide(getDadJoke) 14 | }) 15 | 16 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | }; 5 | outputs = {nixpkgs, ...}: let 6 | forAllSystems = function: 7 | nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed ( 8 | system: function nixpkgs.legacyPackages.${system} 9 | ); 10 | in { 11 | formatter = forAllSystems (pkgs: pkgs.alejandra); 12 | devShells = forAllSystems (pkgs: { 13 | default = pkgs.mkShell { 14 | packages = with pkgs; [ 15 | corepack 16 | nodejs 17 | ]; 18 | }; 19 | }); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/examples/005_provider-specific-configuration-02.ts: -------------------------------------------------------------------------------- 1 | import { Completions } from "@effect/ai" 2 | import { OpenAiCompletions } from "@effect/ai-openai" 3 | import { Console, Effect } from "effect" 4 | 5 | // ┌─── Effect 6 | // ▼ 7 | const getDadJoke = Effect.gen(function*() { 8 | const completions = yield* Completions.Completions 9 | return yield* completions.create("Generate a hilarious dad joke").pipe( 10 | Effect.andThen((response) => Console.log(response.text)), 11 | OpenAiCompletions.withConfigOverride({ 12 | frequency_penalty: 1.5, 13 | temperature: 1.2 14 | }) 15 | ) 16 | }) 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.formatOnSave": true, 4 | "eslint.format.enable": true, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 7 | "editor.formatOnSave": true 8 | }, 9 | "[javascriptreact]": { 10 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 11 | "editor.formatOnSave": true 12 | }, 13 | "[typescript]": { 14 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 15 | "editor.formatOnSave": true 16 | }, 17 | "[typescriptreact]": { 18 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 19 | "editor.formatOnSave": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1740916017, 6 | "narHash": "sha256-Ze/3XCElkVljFCnBKezLldOz2ZaGp7eozxRqFzACnMI=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "b58e19b11fe72175fd7a9e014a4786a91e99da5f", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /src/examples/002_using-provider-integrations-01.ts: -------------------------------------------------------------------------------- 1 | import { OpenAiCompletions } from "@effect/ai-openai" 2 | import { Effect } from "effect" 3 | 4 | // Create an `AiModel` which will provide an implementation of `Completions` 5 | // and requires an `OpenAiClient` 6 | // 7 | // ┌─── AiModel 8 | // ▼ 9 | const Gpt4o = OpenAiCompletions.model("gpt-4o") 10 | 11 | // When you use a model from a specific provider, that provider's client 12 | // will be added to your requirements 13 | // 14 | // ┌─── Effect 15 | // ▼ 16 | const main = Effect.gen(function*() { 17 | const gpt = yield* Gpt4o 18 | yield* gpt.provide(getDadJoke) 19 | }) 20 | -------------------------------------------------------------------------------- /src/examples/004_planning-interactions-02.ts: -------------------------------------------------------------------------------- 1 | import { AiPlan } from "@effect/ai" 2 | import { OpenAiCompletions } from "@effect/ai-openai" 3 | import { Effect, Schedule } from "effect" 4 | 5 | type DadJokeError = NetworkError | ProviderOutage 6 | 7 | // ┌─── AiPlan 8 | // ▼ 9 | const DadJokePlan = AiPlan.fromModel(OpenAiCompletions.model("gpt-4o"), { 10 | attempts: 3, 11 | schedule: Schedule.exponential("100 millis", 1.5), 12 | while: (error: DadJokeError) => error._tag === "NetworkError" 13 | }) 14 | 15 | // ┌─── Effect 16 | // ▼ 17 | const main = Effect.gen(function*() { 18 | const plan = yield* DadJokePlan 19 | yield* plan.provide(getDadJoke) 20 | }) 21 | -------------------------------------------------------------------------------- /src/examples/002_using-provider-integrations-02.ts: -------------------------------------------------------------------------------- 1 | import { OpenAiCompletions } from "@effect/ai-openai" 2 | import { Effect } from "effect" 3 | 4 | // Create an `AiModel` which will provide an implementation of `Completions` 5 | // and requires an `OpenAiClient` 6 | // 7 | // ┌─── AiModel 8 | // ▼ 9 | const Gpt4o = OpenAiCompletions.model("gpt-4o") 10 | 11 | // When you use a model from a specific provider, that provider's client 12 | // will be added to your requirements 13 | // 14 | // ┌─── Effect 15 | // ▼ 16 | const main = Effect.gen(function*() { 17 | const gpt = yield* Gpt4o 18 | yield* gpt.provide(getDadJoke) 19 | yield* gpt.provide(getDadJoke) 20 | yield* gpt.provide(getDadJoke) 21 | }) 22 | -------------------------------------------------------------------------------- /src/examples/002_using-provider-integrations-03.ts: -------------------------------------------------------------------------------- 1 | import { AnthropicCompletions } from "@effect/ai-anthropic" 2 | import { OpenAiCompletions } from "@effect/ai-openai" 3 | import { Effect } from "effect" 4 | 5 | const Claude37 = AnthropicCompletions.model("claude-3-7-sonnet-latest") 6 | const Gpt4o = OpenAiCompletions.model("gpt-4o") 7 | 8 | // When you use a model from a specific provider, that provider's client 9 | // will be added to your requirements 10 | // 11 | // ┌─── Effect 12 | // ▼ 13 | const main = Effect.gen(function*() { 14 | const claude = yield* Claude37 15 | const gpt = yield* Gpt4o 16 | yield* gpt.provide(getDadJoke) 17 | yield* gpt.provide(getDadJoke) 18 | yield* claude.provide(getDadJoke) 19 | }) 20 | -------------------------------------------------------------------------------- /src/examples/003_creating-provider-clients-01.ts: -------------------------------------------------------------------------------- 1 | import { OpenAiClient } from "@effect/ai-openai" 2 | import { NodeHttpClient } from "@effect/platform-node" 3 | import { Effect, Layer, Redacted } from "effect" 4 | 5 | // Create a `Layer` which produces an `OpenAiClient` and requires 6 | // an `HttpClient`. 7 | // 8 | // ┌─── Layer 9 | // ▼ 10 | const OpenAi = OpenAiClient.layer({ 11 | apiKey: Redacted.make("my-secret-openai-api-key") 12 | }) 13 | 14 | // Provide a platform-specific implementation of `HttpClient` 15 | // 16 | // ┌─── Layer 17 | // ▼ 18 | const OpenAiWithHttp = Layer.provide(OpenAi, NodeHttpClient.layerUndici) 19 | 20 | main.pipe( 21 | Effect.provide(OpenAiWithHttp), 22 | Effect.runPromise 23 | ) 24 | -------------------------------------------------------------------------------- /src/examples/003_creating-provider-clients-02.ts: -------------------------------------------------------------------------------- 1 | import { OpenAiClient } from "@effect/ai-openai" 2 | import { NodeHttpClient } from "@effect/platform-node" 3 | import { Config, Effect, Layer } from "effect" 4 | 5 | // Create a `Layer` which produces an `OpenAiClient` and requires 6 | // an `HttpClient`. 7 | // 8 | // ┌─── Layer 9 | // ▼ 10 | const OpenAi = OpenAiClient.layerConfig({ 11 | apiKey: Config.redacted("OPENAI_API_KEY") 12 | }) 13 | 14 | // Provide a platform-specific implementation of `HttpClient` 15 | // 16 | // ┌─── Layer 17 | // ▼ 18 | const OpenAiWithHttp = Layer.provide(OpenAi, NodeHttpClient.layerUndici) 19 | 20 | main.pipe( 21 | Effect.provide(OpenAiWithHttp), 22 | Effect.runPromise 23 | ) 24 | -------------------------------------------------------------------------------- /src/examples/005_provider-specific-configuration-03.ts: -------------------------------------------------------------------------------- 1 | import { Completions } from "@effect/ai" 2 | import { AnthropicCompletions } from "@effect/ai-anthropic" 3 | import { OpenAiCompletions } from "@effect/ai-openai" 4 | import { Console, Effect } from "effect" 5 | 6 | // ┌─── Effect 7 | // ▼ 8 | const getDadJoke = Effect.gen(function*() { 9 | const completions = yield* Completions.Completions 10 | return yield* completions.create("Generate a hilarious dad joke").pipe( 11 | Effect.andThen((response) => Console.log(response.text)), 12 | AnthropicCompletions.withConfigOverride({ 13 | temperature: 1.4 14 | }), 15 | OpenAiCompletions.withConfigOverride({ 16 | frequency_penalty: 1.5, 17 | temperature: 1.2 18 | }) 19 | ) 20 | }) 21 | -------------------------------------------------------------------------------- /src/examples/005_provider-specific-configuration-04.ts: -------------------------------------------------------------------------------- 1 | import { Completions } from "@effect/ai" 2 | import { AnthropicCompletions } from "@effect/ai-anthropic" 3 | import { OpenAiCompletions } from "@effect/ai-openai" 4 | import { Console, Effect } from "effect" 5 | 6 | const Gpt4o = OpenAiCompletions.model("gpt-4o", { 7 | temperature: 0.8 8 | }) 9 | 10 | // ┌─── Effect 11 | // ▼ 12 | const getDadJoke = Effect.gen(function*() { 13 | const completions = yield* Completions.Completions 14 | return yield* completions.create("Generate a hilarious dad joke").pipe( 15 | Effect.andThen((response) => Console.log(response.text)), 16 | AnthropicCompletions.withConfigOverride({ 17 | temperature: 1.4 18 | }), 19 | OpenAiCompletions.withConfigOverride({ 20 | frequency_penalty: 1.5, 21 | temperature: 1.2 22 | }) 23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /src/examples/002_using-provider-integrations-04.ts: -------------------------------------------------------------------------------- 1 | import { AnthropicCompletions } from "@effect/ai-anthropic" 2 | import { OpenAiCompletions } from "@effect/ai-openai" 3 | import { Effect } from "effect" 4 | 5 | class DadJokes extends Effect.Service()("DadJokes", { 6 | effect: Effect.gen(function*() { 7 | const claude = yield* AnthropicCompletions.model("claude-3-7-sonnet-latest") 8 | const gpt = yield* OpenAiCompletions.model("gpt-4o") 9 | return { 10 | getDadJoke: gpt.provide(getDadJoke), 11 | getReallyGroanInducingDadJoke: claude.provide(getDadJoke) 12 | } 13 | }) 14 | }) {} 15 | 16 | // ┌─── Effect 17 | // ▼ 18 | const main = Effect.gen(function*() { 19 | const dadJokes = yield* DadJokes 20 | yield* dadJokes.getReallyGroanInducingDadJoke 21 | }) 22 | 23 | // ┌─── Layer 24 | // ▼ 25 | DadJokes.Default 26 | -------------------------------------------------------------------------------- /src/examples/004_planning-interactions-03.ts: -------------------------------------------------------------------------------- 1 | import { AiPlan } from "@effect/ai" 2 | import { AnthropicCompletions } from "@effect/ai-anthropic" 3 | import { OpenAiCompletions } from "@effect/ai-openai" 4 | import { Effect, Schedule } from "effect" 5 | 6 | type DadJokeError = NetworkError | ProviderOutage 7 | 8 | // ┌─── AiPlan 9 | // ▼ 10 | const DadJokePlan = AiPlan.fromModel(OpenAiCompletions.model("gpt-4o"), { 11 | attempts: 3, 12 | schedule: Schedule.exponential("100 millis", 1.5), 13 | while: (error: DadJokeError) => error._tag === "NetworkError" 14 | }).pipe(AiPlan.withFallback({ 15 | model: AnthropicCompletions.model("claude-3-7-sonnet-latest"), 16 | while: (error: DadJokeError) => error._tag === "ProviderOutage" 17 | })) 18 | 19 | // ┌─── Effect 20 | // ▼ 21 | const main = Effect.gen(function*() { 22 | const plan = yield* DadJokePlan 23 | yield* plan.provide(getDadJoke) 24 | }) 25 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "exactOptionalPropertyTypes": true, 5 | "moduleDetection": "force", 6 | "composite": true, 7 | "downlevelIteration": true, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": false, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "NodeNext", 15 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 16 | "types": [], 17 | "isolatedModules": true, 18 | "sourceMap": true, 19 | "declarationMap": true, 20 | "noImplicitReturns": false, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": false, 23 | "noFallthroughCasesInSwitch": true, 24 | "noEmitOnError": false, 25 | "noErrorTruncation": false, 26 | "allowJs": false, 27 | "checkJs": false, 28 | "forceConsistentCasingInFileNames": true, 29 | "noImplicitAny": true, 30 | "noImplicitThis": true, 31 | "noUncheckedIndexedAccess": false, 32 | "strictNullChecks": true, 33 | "baseUrl": ".", 34 | "target": "ES2022", 35 | "module": "NodeNext", 36 | "incremental": true, 37 | "removeComments": false, 38 | "plugins": [{ "name": "@effect/language-service" }] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /layouts/about-me.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effect-days-2025-keynote", 3 | "type": "module", 4 | "private": true, 5 | "packageManager": "pnpm@10.6.2+sha512.47870716bea1572b53df34ad8647b42962bc790ce2bf4562ba0f643237d7302a3d6a8ecef9e4bdfc01d23af1969aa90485d4cebb0b9638fa5ef1daef656f6c1b", 6 | "scripts": { 7 | "build": "slidev build", 8 | "dev": "slidev --open", 9 | "export": "slidev export", 10 | "check": "tsc -b tsconfig.json", 11 | "lint": "eslint \"**/{src,test,examples,scripts,dtslint}/**/*.{ts,mjs}\"", 12 | "lint:fix": "pnpm lint --fix" 13 | }, 14 | "dependencies": { 15 | "@iconify-json/carbon": "^1.2.8", 16 | "@slidev/theme-default": "latest", 17 | "vue": "^3.5.13" 18 | }, 19 | "devDependencies": { 20 | "@effect/ai": "^0.12.1", 21 | "@effect/ai-anthropic": "^0.2.1", 22 | "@effect/ai-openai": "^0.15.1", 23 | "@effect/eslint-plugin": "^0.3.0", 24 | "@effect/experimental": "^0.43.1", 25 | "@effect/language-service": "^0.4.0", 26 | "@effect/platform": "^0.79.1", 27 | "@effect/platform-node": "^0.75.1", 28 | "@effect/printer": "^0.41.8", 29 | "@effect/printer-ansi": "^0.41.8", 30 | "@effect/vitest": "^0.19.8", 31 | "@eslint/eslintrc": "^3.3.0", 32 | "@eslint/js": "^9.22.0", 33 | "@slidev/cli": "^51.3.0", 34 | "@types/node": "^22.13.10", 35 | "@vitest/expect": "^3.0.8", 36 | "effect": "^3.13.10", 37 | "eslint": "^9.22.0", 38 | "eslint-import-resolver-typescript": "^3.8.4", 39 | "eslint-plugin-import-x": "^4.6.1", 40 | "eslint-plugin-simple-import-sort": "^12.1.1", 41 | "eslint-plugin-sort-destructure-keys": "^2.0.0", 42 | "tsx": "^4.19.3", 43 | "typescript": "^5.8.2", 44 | "typescript-eslint": "^8.26.1", 45 | "vitest": "^3.0.8" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/workflows/003_parallel.ts: -------------------------------------------------------------------------------- 1 | import { AiInput, Completions } from "@effect/ai" 2 | import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" 3 | import { NodeHttpClient } from "@effect/platform-node" 4 | import { Config, Console, Effect, Layer, String } from "effect" 5 | 6 | const systemPrompt = String.stripMargin( 7 | `|Analyze how market changes will impact this stakeholder group. Provide 8 | |specific impacts and recommended actions.Format with clear sections and 9 | |priorities.` 10 | ) 11 | 12 | const performImpactAnalysis = Effect.fn("performImpactAnalysis")( 13 | function*(stakeholder: string) { 14 | const completions = yield* Completions.Completions 15 | return yield* completions.create(`Stakeholder Group:\n${stakeholder}`).pipe( 16 | AiInput.provideSystem(systemPrompt), 17 | Effect.andThen((response) => 18 | Console.log("=== START ANALYSIS ===").pipe( 19 | Effect.andThen(Console.log(`Stakeholder:\n${stakeholder}\n`)), 20 | Effect.andThen(Console.log(`Analysis:\n${response.text}`)), 21 | Effect.andThen(Console.log("=== END ANALYSIS ===\n")) 22 | ) 23 | ), 24 | Effect.orDie 25 | ) 26 | } 27 | ) 28 | 29 | const program = Effect.gen(function*() { 30 | const gpt4o = yield* OpenAiCompletions.model("gpt-4o") 31 | yield* gpt4o.provide( 32 | Effect.all([ 33 | performImpactAnalysis(String.stripMargin( 34 | `|Customers: 35 | |- Price sensitive 36 | |- Want better tech 37 | |- Environmental concerns` 38 | )), 39 | performImpactAnalysis(String.stripMargin( 40 | `|Investors: 41 | |- Expect growth 42 | |- Want cost control 43 | |- Risk concerns` 44 | )), 45 | performImpactAnalysis(String.stripMargin( 46 | `|Suppliers: 47 | |- Capacity constraints 48 | |- Price pressures 49 | |- Tech transitions` 50 | )) 51 | ], { concurrency: "unbounded" }) 52 | ) 53 | }) 54 | 55 | const OpenAi = OpenAiClient.layerConfig({ 56 | apiKey: Config.redacted("OPENAI_API_KEY") 57 | }).pipe(Layer.provide(NodeHttpClient.layerUndici)) 58 | 59 | program.pipe( 60 | Effect.provide(OpenAi), 61 | Effect.runPromise 62 | ) 63 | -------------------------------------------------------------------------------- /src/demos/ai-toolkit.ts: -------------------------------------------------------------------------------- 1 | import { AiToolkit, Completions } from "@effect/ai" 2 | import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" 3 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" 4 | import { NodeHttpClient } from "@effect/platform-node" 5 | import { Array, Config, Console, Effect, Layer, Schema } from "effect" 6 | 7 | class DadJoke extends Schema.Class("DadJoke")({ 8 | id: Schema.String, 9 | joke: Schema.String 10 | }) {} 11 | 12 | class SearchResponse extends Schema.Class("SearchResponse")({ 13 | results: Schema.Array(DadJoke) 14 | }) {} 15 | 16 | class ICanHazDadJoke extends Effect.Service()("ICanHazDadJoke", { 17 | dependencies: [NodeHttpClient.layerUndici], 18 | effect: Effect.gen(function*() { 19 | const httpClient = yield* HttpClient.HttpClient 20 | const httpClientOk = httpClient.pipe( 21 | HttpClient.filterStatusOk, 22 | HttpClient.mapRequest(HttpClientRequest.prependUrl("https://icanhazdadjoke.com")) 23 | ) 24 | 25 | const search = Effect.fn("ICanHazDadJoke.search")( 26 | function*(params: typeof GetDadJoke.Type) { 27 | return yield* httpClientOk.get("/search", { 28 | acceptJson: true, 29 | urlParams: { ...params } 30 | }).pipe( 31 | Effect.flatMap(HttpClientResponse.schemaBodyJson(SearchResponse)), 32 | Effect.flatMap(({ results }) => Array.head(results)), 33 | Effect.map((joke) => joke.joke), 34 | Effect.scoped, 35 | Effect.orDie 36 | ) 37 | } 38 | ) 39 | 40 | return { 41 | search 42 | } as const 43 | }) 44 | }) {} 45 | 46 | // Create the tool request 47 | class GetDadJoke extends Schema.TaggedRequest()("GetDadJoke", { 48 | payload: { 49 | searchTerm: Schema.String.annotations({ 50 | description: "The search term to use to find dad jokes" 51 | }) 52 | }, 53 | success: Schema.String, 54 | failure: Schema.Never 55 | }, { 56 | description: "Get a hilarious dad joke from the icanhazdadjoke API" 57 | }) {} 58 | 59 | // Add tool requests to an AiToolkit 60 | const DadJokeTools = AiToolkit.empty.add(GetDadJoke) 61 | 62 | // Implement the handlers for each tool 63 | const ToolsLayer = DadJokeTools.implement((handlers) => 64 | Effect.gen(function*() { 65 | const icanhazdadjoke = yield* ICanHazDadJoke 66 | return handlers.handle("GetDadJoke", (params) => icanhazdadjoke.search(params)) 67 | }) 68 | ).pipe(Layer.provide(ICanHazDadJoke.Default)) 69 | 70 | // Use the toolkit 71 | const getDadJoke = Effect.gen(function*() { 72 | const completions = yield* Completions.Completions 73 | const tools = yield* DadJokeTools 74 | return yield* completions.toolkit({ 75 | input: "Generate a hilarious dad joke", 76 | tools 77 | }).pipe( 78 | Effect.andThen((response) => response.value), 79 | Effect.andThen((value) => Console.log(value)) 80 | ) 81 | }) 82 | 83 | const program = Effect.gen(function*() { 84 | const model = yield* OpenAiCompletions.model("gpt-4o") 85 | yield* model.provide(getDadJoke) 86 | }) 87 | 88 | const OpenAiLayer = OpenAiClient.layerConfig({ 89 | apiKey: Config.redacted("OPENAI_API_KEY") 90 | }).pipe(Layer.provide(NodeHttpClient.layerUndici)) 91 | 92 | program.pipe( 93 | Effect.provide([OpenAiLayer, ToolsLayer]), 94 | Effect.runPromise 95 | ) 96 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import * as effectEslint from "@effect/eslint-plugin" 2 | import eslint from "@eslint/js" 3 | import * as tsResolver from "eslint-import-resolver-typescript" 4 | import importPlugin from "eslint-plugin-import-x" 5 | import simpleImportSort from "eslint-plugin-simple-import-sort" 6 | import sortDestructureKeys from "eslint-plugin-sort-destructure-keys" 7 | import * as Path from "node:path" 8 | import * as Url from "node:url" 9 | import tseslint from "typescript-eslint" 10 | 11 | const __filename = Url.fileURLToPath(import.meta.url) 12 | const __dirname = Path.dirname(__filename) 13 | 14 | export default tseslint.config( 15 | { 16 | ignores: ["**/dist", "**/build", "**/docs", "**/*.md"] 17 | }, 18 | eslint.configs.recommended, 19 | tseslint.configs.strict, 20 | importPlugin.flatConfigs.recommended, 21 | importPlugin.flatConfigs.typescript, 22 | effectEslint.configs.dprint, 23 | { 24 | plugins: { 25 | "simple-import-sort": simpleImportSort, 26 | "sort-destructure-keys": sortDestructureKeys 27 | }, 28 | 29 | languageOptions: { 30 | parser: tseslint.parser, 31 | ecmaVersion: 2018, 32 | sourceType: "module" 33 | }, 34 | 35 | settings: { 36 | "import-x/resolver": { 37 | name: "tsResolver", 38 | resolver: tsResolver, 39 | options: { 40 | alwaysTryTypes: true 41 | } 42 | } 43 | }, 44 | 45 | rules: { 46 | "no-fallthrough": "off", 47 | "no-irregular-whitespace": "off", 48 | "object-shorthand": "error", 49 | "prefer-destructuring": "off", 50 | "sort-imports": "off", 51 | 52 | "no-restricted-syntax": [ 53 | "error", 54 | { 55 | selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", 56 | message: "Do not use spread arguments in Array.push" 57 | } 58 | ], 59 | 60 | "no-unused-vars": "off", 61 | "require-yield": "off", 62 | "prefer-rest-params": "off", 63 | "prefer-spread": "off", 64 | "import-x/export": "off", 65 | "import-x/first": "error", 66 | "import-x/newline-after-import": "error", 67 | "import-x/no-duplicates": "error", 68 | "import-x/no-named-as-default-member": "off", 69 | "import-x/no-unresolved": "off", 70 | "import-x/order": "off", 71 | "simple-import-sort/imports": "off", 72 | "sort-destructure-keys/sort-destructure-keys": "error", 73 | "deprecation/deprecation": "off", 74 | 75 | "@typescript-eslint/array-type": [ 76 | "warn", 77 | { 78 | default: "generic", 79 | readonly: "generic" 80 | } 81 | ], 82 | "@typescript-eslint/ban-ts-comment": "off", 83 | "@typescript-eslint/ban-types": "off", 84 | "@typescript-eslint/camelcase": "off", 85 | "@typescript-eslint/explicit-module-boundary-types": "off", 86 | "@typescript-eslint/consistent-type-imports": "warn", 87 | "@typescript-eslint/explicit-function-return-type": "off", 88 | "@typescript-eslint/interface-name-prefix": "off", 89 | "@typescript-eslint/member-delimiter-style": 0, 90 | "@typescript-eslint/no-array-constructor": "off", 91 | "@typescript-eslint/no-explicit-any": "off", 92 | "@typescript-eslint/no-empty-interface": "off", 93 | "@typescript-eslint/no-empty-object-type": "off", 94 | "@typescript-eslint/no-invalid-void-type": "off", 95 | "@typescript-eslint/no-namespace": "off", 96 | "@typescript-eslint/no-non-null-assertion": "off", 97 | "@typescript-eslint/no-unsafe-function-type": "off", 98 | "@typescript-eslint/no-unused-vars": [ 99 | "error", 100 | { 101 | argsIgnorePattern: "^_", 102 | varsIgnorePattern: "^_" 103 | } 104 | ], 105 | "@typescript-eslint/no-use-before-define": "off", 106 | "@typescript-eslint/prefer-for-of": "off", 107 | "@typescript-eslint/unified-signatures": "off", 108 | 109 | "@effect/dprint": [ 110 | "error", 111 | { 112 | config: { 113 | indentWidth: 2, 114 | lineWidth: 120, 115 | semiColons: "asi", 116 | quoteStyle: "alwaysDouble", 117 | trailingCommas: "never", 118 | operatorPosition: "maintain", 119 | "arrowFunction.useParentheses": "force" 120 | } 121 | } 122 | ] 123 | } 124 | } 125 | ) 126 | -------------------------------------------------------------------------------- /src/workflows/004_orchestrator_workers.ts: -------------------------------------------------------------------------------- 1 | import { AiInput, Completions } from "@effect/ai" 2 | import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" 3 | import { NodeHttpClient } from "@effect/platform-node" 4 | import { Config, Console, Effect, Layer, Schema, String } from "effect" 5 | 6 | const orchestratorSystemPrompt = String.stripMargin( 7 | `|Analyze the user-provided task and break it down into 2-3 distinct 8 | |approaches. 9 | | 10 | |Return your response in the following JSON format: 11 | | 12 | |\`\`\`json 13 | |{ 14 | | "analysis": "", 15 | | "tasks": [ 16 | | { 17 | | "type": "formal", 18 | | "description": "" 19 | | }, 20 | | { 21 | | "type": "conversational", 22 | | "description": "" 23 | | } 24 | | ] 25 | |} 26 | |\`\`\`` 27 | ) 28 | 29 | const workerSystemPrompt = String.stripMargin( 30 | `|Generate content based on the following user-defined elements. Your content 31 | |should maintain the specified style and fully address all requirements.` 32 | ) 33 | 34 | class Task extends Schema.Class("Task")({ 35 | type: Schema.Literal("formal", "conversational"), 36 | description: Schema.String.annotations({ 37 | description: "If the \"type\" is \"formal\", write a precise, technical " + 38 | "version that emphasizes specifications. If the task \"type\" is " + 39 | "\"conversational\", write an engaging, friendly version that connects with readers" 40 | }) 41 | }) { 42 | static Array = Schema.Array(this) 43 | static encodeTasks = Schema.encodeSync(Schema.parseJson(this.Array, { 44 | space: 2 45 | })) 46 | } 47 | 48 | class TaskList extends Schema.Class("TaskList")({ 49 | analysis: Schema.String.annotations({ 50 | description: "Represents your explanation of your understanding of the " + 51 | "task and which variations would be valuable - focus on how each approach " + 52 | "serves different aspects of the task" 53 | }), 54 | tasks: Task.Array 55 | }, { 56 | description: "An object containing an initial task analysis along with both a " + 57 | "formal and conversational description of the task" 58 | }) {} 59 | 60 | const OpenAi = OpenAiClient.layerConfig({ 61 | apiKey: Config.redacted("OPENAI_API_KEY") 62 | }).pipe(Layer.provide(NodeHttpClient.layerUndici)) 63 | 64 | class FlexibleOrchestrator extends Effect.Service()("FlexibleOrchestrator", { 65 | dependencies: [OpenAi], 66 | effect: Effect.gen(function*() { 67 | const gpt4o = yield* OpenAiCompletions.model("gpt-4o") 68 | 69 | const generateTaskList = Effect.fn("FlexibleOrchestrator.generateTaskList")( 70 | function*(task: string) { 71 | const completions = yield* Completions.Completions 72 | return yield* completions.structured({ 73 | input: `Task: ${task}`, 74 | schema: TaskList 75 | }).pipe( 76 | AiInput.provideSystem(orchestratorSystemPrompt), 77 | Effect.andThen((response) => response.value), 78 | Effect.tapErrorTag("NoSuchElementException", () => Console.error("[ERROR]: Malformed task list")), 79 | Effect.orDie 80 | ) 81 | } 82 | ) 83 | 84 | const process = Effect.fn("FlexibleOrchestrator.process")( 85 | function*(task: string) { 86 | const completions = yield* Completions.Completions 87 | 88 | // Step 1: Get orchestrator response 89 | const response = yield* generateTaskList(task) 90 | yield* Console.log("=== ORCHESTRATOR OUTPUT ===\n") 91 | yield* Console.log(`ANALYSIS:\n\n${response.analysis}\n`) 92 | yield* Console.log(`TASKS:\n\n${Task.encodeTasks(response.tasks)}\n`) 93 | 94 | // Step 2: Process each task concurrently 95 | yield* Effect.forEach( 96 | response.tasks, 97 | (taskInfo) => 98 | completions.create(String.stripMargin( 99 | `|- Task: ${task} 100 | |- Style: ${taskInfo.type} 101 | |- Guidelines: ${taskInfo.description}` 102 | )).pipe( 103 | AiInput.provideSystem(workerSystemPrompt), 104 | Effect.andThen((response) => 105 | Console.log(`=== WORKER RESULT (${taskInfo.type}) ===\n\n${response.text}\n`) 106 | ) 107 | ), 108 | { concurrency: "unbounded", discard: true } 109 | ) 110 | } 111 | ) 112 | 113 | return { 114 | process: (task: string) => gpt4o.provide(process(task)) 115 | } as const 116 | }) 117 | }) {} 118 | 119 | const program = Effect.gen(function*() { 120 | const orchestrator = yield* FlexibleOrchestrator 121 | const task = "Write a product description for a new eco-friendly water bottle" 122 | yield* orchestrator.process(task) 123 | }) 124 | 125 | program.pipe( 126 | Effect.provide([FlexibleOrchestrator.Default]), 127 | Effect.runPromise 128 | ) 129 | -------------------------------------------------------------------------------- /src/workflows/001_sequential.ts: -------------------------------------------------------------------------------- 1 | import { AiInput, Completions } from "@effect/ai" 2 | import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" 3 | import { NodeHttpClient } from "@effect/platform-node" 4 | import { Config, Console, Effect, Layer, String } from "effect" 5 | 6 | const inputText = `Q3 Performance Summary: 7 | Our customer satisfaction score rose to 92 points this quarter. 8 | Revenue grew by 45% compared to last year. 9 | Market share is now at 23% in our primary market. 10 | Customer churn decreased to 5% from 8%. 11 | New user acquisition cost is $43 per user. 12 | Product adoption rate increased to 78%. 13 | Employee satisfaction is at 87 points. 14 | Operating margin improved to 34%.` 15 | 16 | // Define the steps to execute as part of the workflow 17 | function extract(input: string) { 18 | return Effect.gen(function*() { 19 | const completions = yield* Completions.Completions 20 | return yield* completions.create(input).pipe( 21 | AiInput.provideSystem( 22 | String.stripMargin( 23 | `|Extract only the numerical values and their associated metrics from 24 | |the text. Format each as 'value: metric' on a new line. 25 | | 26 | |Example format: 27 | |92: customer satisfaction 28 | |45%: revenue growth` 29 | ) 30 | ) 31 | ) 32 | }) 33 | } 34 | 35 | function sanitize(input: string) { 36 | return Effect.gen(function*() { 37 | const completions = yield* Completions.Completions 38 | return yield* completions.create(input).pipe( 39 | AiInput.provideSystem( 40 | String.stripMargin( 41 | `|Convert all numerical values to percentages where possible. If not 42 | |a percentage or points, convert to decimal. For example, 92 points 43 | |should be converted to 92% and $43 should be converted to 43.00.Keep 44 | |one number per line. 45 | | 46 | |Example format: 47 | |92%: customer satisfaction 48 | |45%: revenue growth` 49 | ) 50 | ) 51 | ) 52 | }) 53 | } 54 | 55 | function sort(input: string) { 56 | return Effect.gen(function*() { 57 | const completions = yield* Completions.Completions 58 | return yield* completions.create(input).pipe( 59 | AiInput.provideSystem( 60 | String.stripMargin( 61 | `|Sort all lines in descending order by numerical value. Keep the 62 | |format 'value: metric' on each line. 63 | | 64 | |Example: 65 | |92%: customer satisfaction 66 | |87%: employee satisfaction` 67 | ) 68 | ) 69 | ) 70 | }) 71 | } 72 | 73 | function format(input: string) { 74 | return Effect.gen(function*() { 75 | const completions = yield* Completions.Completions 76 | return yield* completions.create(input).pipe( 77 | AiInput.provideSystem( 78 | String.stripMarginWith( 79 | `%Format the sorted data as a markdown table with columns: 80 | % | Metric | Value | 81 | % |:--|--:| 82 | % | Customer Satisfaction | 92% |`, 83 | "%" 84 | ) 85 | ) 86 | ) 87 | }) 88 | } 89 | 90 | // Define the models we want to use 91 | const Gpt4oMini = OpenAiCompletions.model("gpt-4o-mini") 92 | const Gpt4o = OpenAiCompletions.model("gpt-4o") 93 | 94 | // Because we used an OpenAi implementation of `Completions`, our 95 | // program will now require us to provide an `OpenAiClient` 96 | // 97 | // ┌─── Effect 98 | // ▼ 99 | const program = Effect.gen(function*() { 100 | const gpt4oMini = yield* Gpt4oMini 101 | const gpt4o = yield* Gpt4o 102 | 103 | yield* Console.log(`Input text: ${inputText}\n`) 104 | 105 | yield* Console.log("Step 1.") 106 | const extracted = yield* gpt4o.provide(extract(inputText)) 107 | yield* Console.log(extracted.text) 108 | 109 | yield* Console.log("\nStep 2.") 110 | const sanitized = yield* gpt4oMini.provide(sanitize(extracted.text)) 111 | yield* Console.log(sanitized.text) 112 | 113 | yield* Console.log("\nStep 3.") 114 | const sorted = yield* gpt4oMini.provide(sort(sanitized.text)) 115 | yield* Console.log(sorted.text) 116 | 117 | yield* Console.log("\nStep 4.") 118 | const formatted = yield* gpt4oMini.provide(format(sorted.text)) 119 | yield* Console.log(formatted.text) 120 | }) 121 | 122 | // Create a `Layer` that produces an OpenAi client 123 | // 124 | // ┌─── Layer 125 | // ▼ 126 | const OpenAi = OpenAiClient.layerConfig({ 127 | apiKey: Config.redacted("OPENAI_API_KEY") 128 | }) 129 | 130 | // The OpenAi client communicates with the OpenAi API via HTTP 131 | // so we need to provide it with an `HttpClient` 132 | // 133 | // ┌─── Layer 134 | // ▼ 135 | const OpenAiHttp = Layer.provide(OpenAi, NodeHttpClient.layerUndici) 136 | 137 | // Run the program 138 | program.pipe( 139 | Effect.provide(OpenAiHttp), 140 | Effect.runPromise 141 | ) 142 | -------------------------------------------------------------------------------- /src/workflows/002_routing.ts: -------------------------------------------------------------------------------- 1 | import { AiInput, AiPlan, Completions } from "@effect/ai" 2 | import { AnthropicClient, AnthropicCompletions } from "@effect/ai-anthropic" 3 | import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" 4 | import { NodeHttpClient } from "@effect/platform-node" 5 | import { Config, Console, Effect, Layer, Schema, Stream, String } from "effect" 6 | 7 | const SupportTeam = Schema.Literal("billing", "technical", "account", "product") 8 | type SupportTeam = typeof SupportTeam.Type 9 | 10 | const promptByTeam: Record = { 11 | billing: String.stripMargin( 12 | `|You are a billing support specialist. Follow these guidelines: 13 | | 1. Always start with "Billing Support Response:" 14 | | 2. First acknowledge the specific billing issue 15 | | 3. Explain any charges or discrepancies clearly 16 | | 4. List concrete next steps with timeline 17 | | 5. End with payment options if relevant 18 | |Keep responses professional but friendly.` 19 | ), 20 | technical: String.stripMargin( 21 | `|You are a technical support engineer. Follow these guidelines: 22 | | 1. Always start with "Technical Support Response:" 23 | | 2. List exact steps to resolve the issue 24 | | 3. Include system requirements if relevant 25 | | 4. Provide workarounds for common problems 26 | | 5. End with escalation path if needed 27 | |Use clear, numbered steps and technical details.` 28 | ), 29 | account: String.stripMargin( 30 | `|You are an account security specialist. Follow these guidelines: 31 | | 1. Always start with "Account Support Response:" 32 | | 2. Prioritize account security and verification 33 | | 3. Provide clear steps for account recovery/changes 34 | | 4. Include security tips and warnings 35 | | 5. Set clear expectations for resolution time 36 | |Maintain a serious, security-focused tone.` 37 | ), 38 | product: String.stripMargin( 39 | `|You are a product specialist. Follow these guidelines: 40 | | 1. Always start with "Product Support Response:" 41 | | 2. Focus on feature education and best practices 42 | | 3. Include specific examples of usage 43 | | 4. Link to relevant documentation sections 44 | | 5. Suggest related features that might help 45 | |Be educational and encouraging in tone.` 46 | ) 47 | } 48 | 49 | export class RoutingResponse extends Schema.Class("RoutingResponse")({ 50 | rationale: Schema.String, 51 | selectedTeam: SupportTeam 52 | }) {} 53 | 54 | function selectTeam(ticket: string) { 55 | return Effect.gen(function*() { 56 | const completions = yield* Completions.Completions 57 | return yield* completions.structured({ 58 | input: ticket, 59 | schema: RoutingResponse 60 | }).pipe( 61 | AiInput.provideSystem( 62 | String.stripMargin( 63 | `|Analyze the input and select the most appropriate support team from 64 | |these options: ${Object.keys(promptByTeam).join(", ")}. Explain your 65 | |reasoning, and provide your selection in JSON format as follows: 66 | |\`\`\`json 67 | |{ 68 | | "rationale":"", 69 | | "team":"" 70 | |} 71 | |\`\`\`` 72 | ) 73 | ) 74 | ) 75 | }) 76 | } 77 | 78 | function routeTicket(ticket: string, route: SupportTeam) { 79 | return Effect.gen(function*() { 80 | const completions = yield* Completions.Completions 81 | const system = promptByTeam[route] 82 | return completions.stream(ticket).pipe( 83 | Stream.provideService(AiInput.SystemInstruction, system) 84 | ) 85 | }).pipe(Stream.unwrap) 86 | } 87 | 88 | // Simple model for responding to tickets 89 | const Gpt4oMini = OpenAiCompletions.model("gpt-4o-mini") 90 | 91 | // For categorizing tickets, we want a robust plan of execution with 92 | // higher-quality models. 93 | // 94 | // The following `TicketResponsePlan` will: 95 | // 1. First attempt to utilize OpenAi's Chat Completions API 96 | // 2. Fallback to Anthropic's Messages API after two attempts to OpenAi 97 | // 3. Fail completely after three attempts to Anthropic 98 | const RoutingPlan = AiPlan.fromModel(OpenAiCompletions.model("gpt-4o"), { 99 | attempts: 2 100 | }).pipe(AiPlan.withFallback({ 101 | model: AnthropicCompletions.model("claude-3-7-sonnet-latest"), 102 | attempts: 3 103 | })) 104 | 105 | // Because we use both OpenAi and Anthropic `Completions`, our 106 | // program will require both an `AnthropicClient` and an `OpenAiClient` 107 | // 108 | // ┌─── Effect 109 | // ▼ 110 | const program = Effect.gen(function*() { 111 | const gpt4oMini = yield* Gpt4oMini 112 | const routingPlan = yield* RoutingPlan 113 | 114 | const ticket = String.stripMargin( 115 | `|Subject: Can't access my account 116 | |Message: Hi, I've been trying to log in for the past hour but keep 117 | |getting an 'invalid password' error. I'm sure I'm using the right 118 | |password. Can you help me regain access? This is urgent as I need to 119 | |submit a report by end of day. 120 | |- John` 121 | ) 122 | 123 | const response = yield* routingPlan.provide(selectTeam(ticket)).pipe( 124 | Effect.andThen((response) => response.value), 125 | Effect.orDie 126 | ) 127 | yield* Console.log(`Selected Route: ${response.selectedTeam}`) 128 | yield* Console.log(`Rationale: ${response.rationale}`) 129 | 130 | yield* routeTicket(ticket, response.selectedTeam).pipe( 131 | Stream.runForEach((response) => 132 | Effect.sync(() => { 133 | process.stdout.write(response.text) 134 | }) 135 | ), 136 | gpt4oMini.provide 137 | ) 138 | }) 139 | 140 | // Create a `Layer` which provides an OpenAi client 141 | const OpenAi = OpenAiClient.layerConfig({ 142 | apiKey: Config.redacted("OPENAI_API_KEY") 143 | }).pipe(Layer.provide(NodeHttpClient.layerUndici)) 144 | 145 | // Create a `Layer` which provides an Anthropic client 146 | const Anthropic = AnthropicClient.layerConfig({ 147 | apiKey: Config.redacted("ANTHROPIC_API_KEY") 148 | }).pipe(Layer.provide(NodeHttpClient.layerUndici)) 149 | 150 | program.pipe( 151 | Effect.provide([OpenAi, Anthropic]), 152 | Effect.runPromise 153 | ) 154 | -------------------------------------------------------------------------------- /src/workflows/005_evaluator_optimizer.ts: -------------------------------------------------------------------------------- 1 | import { AiInput, Completions } from "@effect/ai" 2 | import { AnthropicClient, AnthropicCompletions } from "@effect/ai-anthropic" 3 | import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" 4 | import { NodeHttpClient } from "@effect/platform-node" 5 | import { Config, Console, Effect, Layer, Schema, String } from "effect" 6 | 7 | const generatorSystemPrompt = String.stripMargin( 8 | `|Your goal is to complete the user-provided task. If there is any feedback 9 | |from your previous generations, you should reflect on them and use them to 10 | |improve your solution. 11 | | 12 | |Output your answer concisely in the following JSON format: 13 | | 14 | |\`\`\`json 15 | |{ 16 | | "thoughts": "", 17 | | "solution": "" 18 | |} 19 | |\`\`\`` 20 | ) 21 | 22 | const evaluatorSystemPrompt = String.stripMargin( 23 | `|Evaluate this following code implementation for: 24 | |1. code correctness 25 | |2. time complexity 26 | |3. style and best practices 27 | | 28 | |Requirements: 29 | |- You should be evaluating only and not attemping to solve the task 30 | |- Only output "PASS" if all criteria are met and you have no further 31 | | suggestions for improvements 32 | | 33 | |Output your evaluation concisely in the following JSON format: 34 | | 35 | |\`\`\`json 36 | |{ 37 | | "evaluation": "", 38 | | "feedback": "" 39 | |} 40 | |\`\`\`` 41 | ) 42 | 43 | class Generation extends Schema.Class("Generation")({ 44 | thoughts: Schema.String.annotations({ 45 | description: "Your understanding of the task and feedback and how you plan to improve" 46 | }), 47 | solution: Schema.String.annotations({ 48 | description: "Your solution to the task written in TypeScript code" 49 | }) 50 | }) {} 51 | 52 | class Evaluation extends Schema.Class("Evaluation")({ 53 | status: Schema.Literal("PASS", "NEEDS_IMPROVEMENT", "FAIL").annotations({ 54 | description: "PASS if the solution meets all requirements, NEEDS_IMPROVEMENT " + 55 | "if the solution partially meets the requirements but requires some adjustment " + 56 | "or FAIL if the solution does not meet any of the requirements" 57 | }), 58 | feedback: Schema.String.annotations({ 59 | description: "What parts of the solution need improvement and why" 60 | }) 61 | }) {} 62 | 63 | const generate = Effect.fn("generate")( 64 | function*(task: string, context: string = "") { 65 | const completions = yield* Completions.Completions 66 | 67 | // Prepend context to the prompt if present 68 | const prompt = `${context.length === 0 ? "" : context + "\n"}Task: ${task}` 69 | 70 | const response = yield* completions.structured({ 71 | input: prompt, 72 | schema: Generation 73 | }).pipe( 74 | AiInput.provideSystem(generatorSystemPrompt), 75 | Effect.andThen((response) => response.value), 76 | Effect.tapErrorTag("NoSuchElementException", () => Console.error("[ERROR]: Malformed generation")), 77 | Effect.orDie 78 | ) 79 | 80 | yield* Console.log("=== GENERATION START ===") 81 | yield* Console.log(`Thoughts:\n${response.thoughts}\n`) 82 | yield* Console.log(`Solution:\n${response.solution}`) 83 | yield* Console.log("=== GENERATION END ===\n") 84 | 85 | return response 86 | } 87 | ) 88 | 89 | const evaluate = Effect.fn("evaluate")( 90 | function*(task: string, solution: string) { 91 | const completions = yield* Completions.Completions 92 | 93 | const prompt = `Original Task: ${task}\nSolution to Evaluate: ${solution}` 94 | 95 | const response = yield* completions.structured({ 96 | input: prompt, 97 | schema: Evaluation 98 | }).pipe( 99 | AiInput.provideSystem(evaluatorSystemPrompt), 100 | Effect.andThen((response) => response.value), 101 | Effect.tapErrorTag("NoSuchElementException", () => Console.error("[ERROR]: Malformed evaluation")), 102 | Effect.orDie 103 | ) 104 | 105 | yield* Console.log("=== EVALUATION START ===") 106 | yield* Console.log(`Status: ${response.status}\n`) 107 | yield* Console.log(`Feedback:\n${response.feedback}`) 108 | yield* Console.log("=== EVALUATION END ===\n") 109 | 110 | return response 111 | } 112 | ) 113 | 114 | const program = Effect.gen(function*() { 115 | // Use a lower fidelity model for the generations (to try to force a few generations) 116 | const claude = yield* AnthropicCompletions.model("claude-3-5-haiku-latest") 117 | // Use a higher fidelity model for evaluations 118 | const gpt4o = yield* OpenAiCompletions.model("gpt-4o") 119 | 120 | const task = String.stripMargin( 121 | `|Task: 122 | | 123 | |Implement a Stack data structure in TypeScript meant to store numbers 124 | |which implements the following methods: 125 | |1. push(x) 126 | |2. pop() 127 | |3. getMin() 128 | | 129 | |All operations should be O(1)` 130 | ) 131 | 132 | const memory: Array = [] 133 | 134 | // Create the initial generation and add it to memory 135 | const generation = yield* claude.provide(generate(task)) 136 | memory.push(generation.solution) 137 | 138 | // Loop until we find a solution (can setup a maximum number of generations, if desired) 139 | while (true) { 140 | // Evaluate the solution 141 | const evaluation = yield* gpt4o.provide(evaluate(task, generation.solution)) 142 | 143 | if (evaluation.status === "PASS") { 144 | yield* Console.log("=== FINAL SOLUTION ===") 145 | yield* Console.log(`${generation.solution}`) 146 | yield* Console.log("=== FINAL SOLUTION ===\n") 147 | break 148 | } 149 | 150 | // Provide previous solutions and feedback to the next generation attempt 151 | const previousAttempts = memory.map((solution) => `- ${solution}\n`) 152 | const context = String.stripMargin( 153 | `|Previous Attempts: 154 | |${previousAttempts} 155 | | 156 | |Feedback: 157 | |${evaluation.feedback}` 158 | ) 159 | 160 | // Perform the next generation with the added context 161 | const nextGeneration = yield* claude.provide(generate(task, context)) 162 | memory.push(nextGeneration.solution) 163 | } 164 | }) 165 | 166 | const Anthropic = AnthropicClient.layerConfig({ 167 | apiKey: Config.redacted("ANTHROPIC_API_KEY") 168 | }).pipe(Layer.provide(NodeHttpClient.layerUndici)) 169 | 170 | const OpenAi = OpenAiClient.layerConfig({ 171 | apiKey: Config.redacted("OPENAI_API_KEY") 172 | }).pipe(Layer.provide(NodeHttpClient.layerUndici)) 173 | 174 | program.pipe( 175 | Effect.provide([Anthropic, OpenAi]), 176 | Effect.runPromise 177 | ) 178 | -------------------------------------------------------------------------------- /slides.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Keynote | Effect Days 2025 3 | favicon: /favicon.png 4 | theme: default 5 | fonts: 6 | sans: Inter 7 | serif: Inter 8 | mono: JetBrains Mono 9 | colorSchema: dark 10 | class: cursor-default 11 | --- 12 | 13 | # Building Effect-ive Agents 14 | 15 | Exploring agentic systems with Effect 16 | 17 | 27 | 28 | --- 29 | layout: about-me 30 | --- 31 | 32 | 40 | 41 | --- 42 | layout: center 43 | --- 44 | 45 | # What We'll Cover Today 46 | 47 | 48 | 49 | - Why use Effect to build agentic systems? 50 | - What makes building an agentic system complex? 51 | - What even is an agentic system? 52 | - How do we use Effect to build these systems? 53 | 54 | 55 | 56 | 72 | 73 | --- 74 | layout: center 75 | --- 76 | 77 | # Why Use Effect? 78 | 79 |
80 | A meme depicting Drake reacting negatively to React's useEffect but positively to the phrase 'Use Effect' 85 |
86 | 87 | 97 | 98 | --- 99 | layout: statement 100 | --- 101 | 102 |

103 | Developing any production-grade system is hard! 104 |

105 | 106 | 130 | 131 | --- 132 | layout: fact 133 | --- 134 | 135 |

136 | Non-Determinism 137 |

138 | 139 | 151 | 152 | --- 153 | layout: center 154 | --- 155 | 156 |
157 | 158 | > In systems where non-determinism is the default, Effect is the antidote 159 | 160 | - Maxwell Brown - Effect Meetup 2024, San Francisco 161 | 162 |
163 | 164 | 179 | 180 | --- 181 | layout: center 182 | --- 183 | 184 |
185 | A modern, high-contrast illustration of the Effect Bot as a janitor cleaning up extreme chaos in an AI development workspace 190 |
191 | 192 | 213 | 214 | --- 215 | layout: center 216 | --- 217 | 218 |
219 | 220 |
221 | 222 | # What is an agentic system? 223 | 224 |
225 | 226 |
227 | 228 |
229 | 230 | ### Workflows 231 | 232 | - Execution is Pre-Determined 233 | - Minimal Dynamicism 234 | 235 |
236 | 237 |
238 | 239 | ### Agents 240 | 241 | - Execution is Self-Directed 242 | - Maximum Dynamicism 243 | 244 |
245 | 246 |
247 | 248 |
249 | 250 | 265 | 266 | --- 267 | layout: center 268 | --- 269 | 270 | # The Augmented LLM 271 | 272 |
273 | A flowchart depicting an LLM with augmented capabilities, namely retrieval, tools, and memory 278 |
279 | 280 |

281 | Image source: Anthropic. (2024). Building Effective Agents. https://www.anthropic.com/engineering/building-effective-agents 282 |

283 | 284 | 310 | 311 | --- 312 | layout: center 313 | --- 314 | 315 |

316 | LLM Interactions with Effect 317 |

318 | 319 | 320 | 321 | ````md magic-move {lines:false} 322 | 323 | ```sh 324 | pnpm add @effect/ai 325 | ``` 326 | 327 | ```sh 328 | pnpm add @effect/ai 329 | pnpm add @effect/ai-anthropic 330 | pnpm add @effect/ai-openai 331 | ``` 332 | 333 | ```` 334 | 335 | 336 | 337 | 353 | 354 | --- 355 | layout: center 356 | --- 357 | 358 | # Describing LLM Interactions 359 | 360 | <<< @/src/examples/001_using-effect-ai-01.ts ts {|1,9|10-12|4-7|} 361 | 362 | 386 | 387 | --- 388 | layout: center 389 | --- 390 | 391 | # Using Provider Integrations 392 | 393 | ````md magic-move 394 | 395 | <<< @/src/examples/002_using-provider-integrations-01.ts ts {|1|4-9|17|18|11-15} 396 | 397 | <<< @/src/examples/002_using-provider-integrations-02.ts ts {17-20} 398 | 399 | <<< @/src/examples/002_using-provider-integrations-03.ts ts {|1,5,14,18|8-12} 400 | 401 | <<< @/src/examples/002_using-provider-integrations-04.ts ts {|7-8|9-12|16-21|23-} 402 | 403 | ```` 404 | 405 | 420 | 421 | 459 | 460 | --- 461 | layout: center 462 | --- 463 | 464 | # Creating Clients for Providers 465 | 466 | ````md magic-move 467 | 468 | <<< @/src/examples/003_creating-provider-clients-01.ts ts {|10-12} 469 | 470 | <<< @/src/examples/003_creating-provider-clients-02.ts ts {10-12|5-9|2,14-18|20-} 471 | 472 | ```` 473 | 474 | 494 | 495 | --- 496 | layout: center 497 | --- 498 | 499 | # Handling Failure Scenarios 500 | 501 | 502 | 503 | 512 | 513 | 522 | 523 | 524 | 525 | 526 | 536 | 537 | --- 538 | layout: center 539 | --- 540 | 541 | # Planning Provider Interactions 542 | 543 | ````md magic-move 544 | 545 | <<< @/src/examples/004_planning-interactions-01.ts ts {|5,7|9-} 546 | 547 | <<< @/src/examples/004_planning-interactions-02.ts ts {1|9|10-12|7-13} 548 | 549 | <<< @/src/examples/004_planning-interactions-03.ts ts {14-17|15|16|8-10,15|19-} 550 | 551 | ```` 552 | 553 | 599 | 600 | --- 601 | layout: center 602 | --- 603 | 604 | # Provider-Specific Configuration 605 | 606 | ````md magic-move 607 | 608 | <<< @/src/examples/005_provider-specific-configuration-01.ts ts 609 | 610 | <<< @/src/examples/005_provider-specific-configuration-02.ts ts {2,11-14} 611 | 612 | <<< @/src/examples/005_provider-specific-configuration-03.ts ts {2,3,12-18} 613 | 614 | <<< @/src/examples/005_provider-specific-configuration-04.ts ts {6-8,19-22} 615 | 616 | ```` 617 | 618 | 636 | 637 | --- 638 | layout: center 639 | --- 640 | 641 | # The Augmented LLM 642 | 643 |
644 | A flowchart depicting an LLM with augmented capabilities, namely retrieval, tools, and memory 649 |
650 | 651 |

652 | Image source: Anthropic. (2024). Building Effective Agents. https://www.anthropic.com/engineering/building-effective-agents 653 |

654 | 655 | 674 | 675 | --- 676 | layout: center 677 | --- 678 | 679 | # Defining a Tool Call 680 | 681 | ```ts twoslash 682 | import { AiToolkit, Completions } from "@effect/ai" 683 | import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" 684 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" 685 | import { NodeHttpClient } from "@effect/platform-node" 686 | import { Array, Config, Console, Effect, Layer, Schema } from "effect" 687 | 688 | class DadJoke extends Schema.Class("DadJoke")({ 689 | id: Schema.String, 690 | joke: Schema.String 691 | }) {} 692 | 693 | class SearchResponse extends Schema.Class("SearchResponse")({ 694 | results: Schema.Array(DadJoke) 695 | }) {} 696 | 697 | class ICanHazDadJoke extends Effect.Service()("ICanHazDadJoke", { 698 | dependencies: [NodeHttpClient.layerUndici], 699 | effect: Effect.gen(function*() { 700 | const httpClient = yield* HttpClient.HttpClient 701 | const httpClientOk = httpClient.pipe( 702 | HttpClient.filterStatusOk, 703 | HttpClient.mapRequest(HttpClientRequest.prependUrl("https://icanhazdadjoke.com")) 704 | ) 705 | 706 | const search = Effect.fn("ICanHazDadJoke.search")( 707 | function*(params: typeof GetDadJoke.Type) { 708 | return yield* httpClientOk.get("/search", { 709 | acceptJson: true, 710 | urlParams: { ...params } 711 | }).pipe( 712 | Effect.flatMap(HttpClientResponse.schemaBodyJson(SearchResponse)), 713 | Effect.flatMap(({ results }) => Array.head(results)), 714 | Effect.map((joke) => joke.joke), 715 | Effect.scoped, 716 | Effect.orDie 717 | ) 718 | } 719 | ) 720 | 721 | return { 722 | search 723 | } as const 724 | }) 725 | }) {} 726 | //---cut--- 727 | // Create the tool request 728 | class GetDadJoke extends Schema.TaggedRequest()("GetDadJoke", { 729 | payload: { 730 | searchTerm: Schema.String.annotations({ 731 | description: "The search term to use to find dad jokes" 732 | }) 733 | }, 734 | success: Schema.String, 735 | failure: Schema.Never 736 | }, { 737 | description: "Get a hilarious dad joke from the icanhazdadjoke API" 738 | }) {} 739 | 740 | // Add tool requests to an AiToolkit 741 | const DadJokeTools = AiToolkit.empty.add(GetDadJoke) 742 | 743 | // Implement the handlers for each tool 744 | const ToolsLayer = DadJokeTools.implement((handlers) => 745 | Effect.gen(function*() { 746 | const icanhazdadjoke = yield* ICanHazDadJoke 747 | return handlers.handle("GetDadJoke", (params) => icanhazdadjoke.search(params)) 748 | }) 749 | ) 750 | //---cut-after--- 751 | .pipe(Layer.provide(ICanHazDadJoke.Default)) 752 | 753 | // Use the toolkit 754 | const getDadJoke = Effect.gen(function*() { 755 | const completions = yield* Completions.Completions 756 | const tools = yield* DadJokeTools 757 | return yield* completions.toolkit({ 758 | input: "Generate a hilarious dad joke", 759 | tools 760 | }).pipe( 761 | Effect.andThen((response) => response.value), 762 | Effect.andThen((value) => Console.log(value)) 763 | ) 764 | }) 765 | 766 | const program = Effect.gen(function*() { 767 | const model = yield* OpenAiCompletions.model("gpt-4o") 768 | yield* model.provide(getDadJoke) 769 | }) 770 | 771 | const OpenAiLayer = OpenAiClient.layerConfig({ 772 | apiKey: Config.redacted("OPENAI_API_KEY") 773 | }).pipe(Layer.provide(NodeHttpClient.layerUndici)) 774 | 775 | program.pipe( 776 | Effect.provide([OpenAiLayer, ToolsLayer]), 777 | Effect.runPromise 778 | ) 779 | ``` 780 | 813 | 814 | --- 815 | layout: center 816 | --- 817 | 818 | # Using Tools 819 | 820 | ```ts twoslash 821 | import { AiToolkit, Completions } from "@effect/ai" 822 | import { OpenAiClient, OpenAiCompletions } from "@effect/ai-openai" 823 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" 824 | import { NodeHttpClient } from "@effect/platform-node" 825 | import { Array, Config, Console, Effect, Layer, Schema } from "effect" 826 | 827 | class DadJoke extends Schema.Class("DadJoke")({ 828 | id: Schema.String, 829 | joke: Schema.String 830 | }) {} 831 | 832 | class SearchResponse extends Schema.Class("SearchResponse")({ 833 | results: Schema.Array(DadJoke) 834 | }) {} 835 | 836 | class ICanHazDadJoke extends Effect.Service()("ICanHazDadJoke", { 837 | dependencies: [NodeHttpClient.layerUndici], 838 | effect: Effect.gen(function*() { 839 | const httpClient = yield* HttpClient.HttpClient 840 | const httpClientOk = httpClient.pipe( 841 | HttpClient.filterStatusOk, 842 | HttpClient.mapRequest(HttpClientRequest.prependUrl("https://icanhazdadjoke.com")) 843 | ) 844 | 845 | const search = Effect.fn("ICanHazDadJoke.search")( 846 | function*(params: typeof GetDadJoke.Type) { 847 | return yield* httpClientOk.get("/search", { 848 | acceptJson: true, 849 | urlParams: { ...params } 850 | }).pipe( 851 | Effect.flatMap(HttpClientResponse.schemaBodyJson(SearchResponse)), 852 | Effect.flatMap(({ results }) => Array.head(results)), 853 | Effect.map((joke) => joke.joke), 854 | Effect.scoped, 855 | Effect.orDie 856 | ) 857 | } 858 | ) 859 | 860 | return { 861 | search 862 | } as const 863 | }) 864 | }) {} 865 | 866 | // Create the tool request 867 | class GetDadJoke extends Schema.TaggedRequest()("GetDadJoke", { 868 | payload: { 869 | searchTerm: Schema.String.annotations({ 870 | description: "The search term to use to find dad jokes" 871 | }) 872 | }, 873 | success: Schema.String, 874 | failure: Schema.Never 875 | }, { 876 | description: "Get a hilarious dad joke from the icanhazdadjoke API" 877 | }) {} 878 | 879 | // Add tool requests to an AiToolkit 880 | const DadJokeTools = AiToolkit.empty.add(GetDadJoke) 881 | 882 | // Implement the handlers for each tool request 883 | const ToolsLayer = DadJokeTools.implement((handlers) => 884 | Effect.gen(function*() { 885 | const icanhazdadjoke = yield* ICanHazDadJoke 886 | return handlers.handle("GetDadJoke", (params) => icanhazdadjoke.search(params)) 887 | }) 888 | ).pipe(Layer.provide(ICanHazDadJoke.Default)) 889 | 890 | // Use the toolkit 891 | //---cut--- 892 | const getDadJoke = Effect.gen(function*() { 893 | const completions = yield* Completions.Completions 894 | const tools = yield* DadJokeTools 895 | return yield* completions.toolkit({ 896 | input: "Generate a hilarious dad joke", 897 | tools 898 | }).pipe( 899 | Effect.andThen((response) => response.value), 900 | Effect.andThen((value) => Console.log(value)) 901 | ) 902 | }) 903 | //---cut-after--- 904 | const program = Effect.gen(function*() { 905 | const model = yield* OpenAiCompletions.model("gpt-4o") 906 | yield* model.provide(getDadJoke) 907 | }) 908 | 909 | const OpenAiLayer = OpenAiClient.layerConfig({ 910 | apiKey: Config.redacted("OPENAI_API_KEY") 911 | }).pipe(Layer.provide(NodeHttpClient.layerUndici)) 912 | 913 | program.pipe( 914 | Effect.provide([OpenAiLayer, ToolsLayer]), 915 | Effect.runPromise 916 | ) 917 | ``` 918 | 919 | 937 | 938 | --- 939 | layout: center 940 | --- 941 | 942 | # Future Directions 943 | 944 | 945 | 946 | - Implementing Provider Services 947 | - Model Context Protocol 948 | - Un-Implemented Features 949 | 950 | 951 | 952 | 966 | 967 | --- 968 | layout: two-cols 969 | --- 970 | 971 |
972 |

Thank You!

973 |
    974 |
  • 975 | 976 | @imax153 977 |
  • 978 |
  • 979 | 980 | github.com/imax153 981 |
  • 982 |
983 |
984 | 985 | ::right:: 986 | 987 |
988 |
989 | A QR code which navigates to the GitHub repository containing the code for this presentation 994 |
995 |

Scan to get the code for this presentation

996 |
997 | 998 | --------------------------------------------------------------------------------