├── .changeset └── config.json ├── .github └── workflows │ └── changeset.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── bun.lockb ├── drizzle.config.ts ├── index.ts ├── package.json ├── src ├── Interfaces │ ├── IImportanceAnalyzer.ts │ └── ILLMProvider.ts ├── agent │ ├── ai │ │ ├── arhieve │ │ │ └── btc-price │ │ │ │ ├── btc-price.ts │ │ │ │ └── index.ts │ │ ├── character │ │ │ └── ducky.ts │ │ ├── config.ts │ │ └── tools │ │ │ └── token.ts │ └── index.ts ├── core │ ├── ai.ts │ ├── goals │ │ ├── context.ts │ │ ├── manger.ts │ │ └── post.ts │ ├── managers │ │ ├── character.ts │ │ ├── coingecko.ts │ │ ├── conversation.ts │ │ ├── fatduck.ts │ │ ├── image.ts │ │ ├── libp2p.ts │ │ ├── llm.ts │ │ ├── memory.ts │ │ ├── preprocess.ts │ │ ├── quantum-personality.ts │ │ ├── quantum.ts │ │ ├── scheduler.ts │ │ ├── style.ts │ │ └── tools.ts │ ├── platform │ │ ├── api │ │ │ └── server.ts │ │ ├── telegram │ │ │ ├── PromptService.ts │ │ │ ├── handlers │ │ │ │ ├── imageHandler.ts │ │ │ │ └── messageHandler.ts │ │ │ └── telegram.ts │ │ └── twitter │ │ │ ├── api │ │ │ └── src │ │ │ │ ├── auth │ │ │ │ └── strategies.ts │ │ │ │ ├── client.ts │ │ │ │ ├── error.ts │ │ │ │ ├── interfaces.ts │ │ │ │ ├── media.ts │ │ │ │ ├── operations │ │ │ │ └── base.ts │ │ │ │ └── strategies │ │ │ │ ├── graphql-request-strategy.ts │ │ │ │ └── request-strategy.ts │ │ │ └── twitter.ts │ ├── services │ │ ├── Event.ts │ │ ├── Interaction.ts │ │ └── quantum-state.ts │ ├── types.ts │ └── utils │ │ ├── IBMRest.ts │ │ ├── logger.ts │ │ └── utils.ts ├── create-character │ ├── base.ts │ ├── builder.ts │ └── types.ts ├── db │ ├── index.ts │ ├── migrate.ts │ ├── migrations │ │ ├── 0000_orange_living_tribunal.sql │ │ ├── 0001_small_cerise.sql │ │ ├── 0002_sturdy_spirit.sql │ │ ├── 0003_mature_phil_sheldon.sql │ │ ├── 0004_numerous_storm.sql │ │ ├── 0005_crazy_raza.sql │ │ ├── 0006_grey_speedball.sql │ │ ├── 0007_useful_darwin.sql │ │ ├── 0008_lean_machine_man.sql │ │ ├── 0009_lowly_the_watchers.sql │ │ ├── 0010_fine_boom_boom.sql │ │ ├── 0011_familiar_prism.sql │ │ ├── 0012_sticky_sway.sql │ │ ├── 0013_furry_squadron_supreme.sql │ │ ├── 0014_fantastic_squadron_supreme.sql │ │ ├── 0015_young_gateway.sql │ │ ├── 0016_wakeful_mastermind.sql │ │ ├── 0017_ancient_rocket_raccoon.sql │ │ ├── 0018_workable_praxagora.sql │ │ ├── 0019_good_patch.sql │ │ ├── 0020_public_guardsmen.sql │ │ ├── 0021_early_spiral.sql │ │ ├── 0022_lumpy_boomer.sql │ │ ├── 0023_bouncy_sally_floyd.sql │ │ ├── 0024_watery_mandrill.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ ├── 0003_snapshot.json │ │ │ ├── 0004_snapshot.json │ │ │ ├── 0005_snapshot.json │ │ │ ├── 0006_snapshot.json │ │ │ ├── 0007_snapshot.json │ │ │ ├── 0008_snapshot.json │ │ │ ├── 0009_snapshot.json │ │ │ ├── 0010_snapshot.json │ │ │ ├── 0011_snapshot.json │ │ │ ├── 0012_snapshot.json │ │ │ ├── 0013_snapshot.json │ │ │ ├── 0014_snapshot.json │ │ │ ├── 0015_snapshot.json │ │ │ ├── 0016_snapshot.json │ │ │ ├── 0017_snapshot.json │ │ │ ├── 0018_snapshot.json │ │ │ ├── 0019_snapshot.json │ │ │ ├── 0020_snapshot.json │ │ │ ├── 0021_snapshot.json │ │ │ ├── 0022_snapshot.json │ │ │ ├── 0023_snapshot.json │ │ │ ├── 0024_snapshot.json │ │ │ └── _journal.json │ └── schema │ │ ├── goal.ts │ │ └── schema.ts ├── docs │ ├── QUANTUM_ELI5.md │ └── flow.MD └── types │ ├── character.ts │ ├── index.ts │ ├── llm.ts │ ├── memory.ts │ ├── style.ts │ └── tools.ts └── tsconfig.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/changeset.yml: -------------------------------------------------------------------------------- 1 | name: Changeset 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | packages: write 11 | issues: write 12 | 13 | concurrency: ${{ github.workflow }}-${{ github.ref }} 14 | 15 | jobs: 16 | release: 17 | name: Release 18 | env: 19 | HUSKY: 0 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout Repo 23 | uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 18 31 | registry-url: "https://registry.npmjs.org" 32 | scope: "@fatduckai" 33 | 34 | - name: Setup Bun 35 | uses: oven-sh/setup-bun@v1 36 | with: 37 | bun-version: latest 38 | 39 | - name: Install Dependencies 40 | run: bun install --frozen-lockfile 41 | 42 | - name: Build Packages 43 | run: bun run build 44 | 45 | - name: Create Release Pull Request or Publish to npm 46 | id: changesets 47 | uses: changesets/action@v1 48 | with: 49 | version: bun run version-packages 50 | publish: bun run release 51 | commit: "chore: update versions" 52 | title: "chore: update versions" 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 55 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 56 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | src/agent/ai/config/* 104 | src/core/config/* 105 | .env 106 | .env2 107 | .env.development.local 108 | .env.test.local 109 | .env.production.local 110 | .env.local 111 | 112 | # parcel-bundler cache (https://parceljs.org/) 113 | 114 | .parcel-cache 115 | 116 | # Next.js build output 117 | 118 | .next 119 | out 120 | 121 | # Nuxt.js build / generate output 122 | 123 | .nuxt 124 | dist 125 | 126 | # Gatsby files 127 | 128 | # Comment in the public line in if your project uses Gatsby and not Next.js 129 | 130 | # https://nextjs.org/blog/next-9-1#public-directory-support 131 | 132 | # public 133 | 134 | # vuepress build output 135 | 136 | .vuepress/dist 137 | 138 | # vuepress v2.x temp and cache directory 139 | 140 | .temp 141 | 142 | # Docusaurus cache and generated files 143 | 144 | .docusaurus 145 | 146 | # Serverless directories 147 | 148 | .serverless/ 149 | 150 | # FuseBox cache 151 | 152 | .fusebox/ 153 | 154 | # DynamoDB Local files 155 | 156 | .dynamodb/ 157 | 158 | # TernJS port file 159 | 160 | .tern-port 161 | 162 | # Stores VSCode versions used for testing VSCode extensions 163 | 164 | .vscode-test 165 | 166 | # yarn v2 167 | 168 | .yarn/cache 169 | .yarn/unplugged 170 | .yarn/build-state.yml 171 | .yarn/install-state.gz 172 | .pnp.* 173 | 174 | # IntelliJ based IDEs 175 | .idea 176 | 177 | # Finder (MacOS) folder config 178 | .DS_Store 179 | 180 | 181 | ./src/core/config/Ducky.json 182 | ./src/core/config/Test.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @fatduckai/ai 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - 857f267: working twitter integration and mention tracking along with timeline review and price 8 | 9 | ### Patch Changes 10 | 11 | - 2d9f9ad: fix token big 12 | 13 | ## 0.8.1 14 | 15 | ### Patch Changes 16 | 17 | - fac2aad: fix bug 18 | 19 | ## 0.8.0 20 | 21 | ### Minor Changes 22 | 23 | - 1430da3: added libp2p comms 24 | 25 | ## 0.7.2 26 | 27 | ### Patch Changes 28 | 29 | - 51f8039: added iamge 30 | 31 | ## 0.7.1 32 | 33 | ### Patch Changes 34 | 35 | - cc05927: fix tools 36 | 37 | ## 0.7.0 38 | 39 | ### Minor Changes 40 | 41 | - 738c5ce: working tg 42 | - cd07c26: working interactions 43 | 44 | ### Patch Changes 45 | 46 | - 09e8ee3: fix ts issue 47 | 48 | ## 0.6.0 49 | 50 | ### Minor Changes 51 | 52 | - 2d6d7a6: updated character creation 53 | 54 | ### Patch Changes 55 | 56 | - 68fbfcf: fix package json 57 | 58 | ## 0.5.0 59 | 60 | ### Minor Changes 61 | 62 | - 958d8ad: fix 63 | 64 | ### Patch Changes 65 | 66 | - 958d8ad: fix 67 | - d547bc9: fic changeset 68 | 69 | ## 0.4.0 70 | 71 | ### Minor Changes 72 | 73 | - 6a8d7e3: fix versions 74 | 75 | ## 0.1.0 76 | 77 | ### Minor Changes 78 | 79 | - f1075f5: refactor 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Character System 2 | 3 | A flexible system for creating and managing AI characters with platform-specific response styles and personality traits. 4 | 5 | ## Features 6 | 7 | - Character creation and management 8 | - Platform-specific response formatting (Twitter, Discord) 9 | - Tool integration for real-time data 10 | - Multiple interaction modes 11 | - Personality and style injection 12 | - Memory management 13 | - Event tracking 14 | 15 | ## Installation 16 | 17 | ```bash 18 | npm install 19 | # or 20 | bun install 21 | ``` 22 | 23 | Create a `.env` file with: 24 | 25 | ```env 26 | DATABASE_URL=your_database_url 27 | OPENAI_API_KEY=your_openai_api_key 28 | ``` 29 | 30 | ## Quick Start 31 | 32 | ```typescript 33 | import { ai } from "../core/ai"; 34 | 35 | const assistant = new ai({ 36 | databaseUrl: process.env.DATABASE_URL!, 37 | llmConfig: { 38 | apiKey: process.env.OPENAI_API_KEY!, 39 | llm: { model: "gpt-4-turbo-preview", temperature: 0.7 }, 40 | analyzer: { model: "gpt-3.5-turbo", temperature: 0.3 }, 41 | }, 42 | }); 43 | ``` 44 | 45 | ## Example Usage 46 | 47 | ### Character Management 48 | 49 | ```typescript 50 | // Find or create character 51 | const characters = await assistant.db 52 | .select() 53 | .from(schema.characters) 54 | .where(eq(schema.characters.name, "Ducky")); 55 | 56 | let character; 57 | if (characters.length > 0) { 58 | character = characters[0]; 59 | } else { 60 | const duckyConfig = await createDuckyCharacter(); 61 | character = await assistant.createCharacter(duckyConfig); 62 | } 63 | ``` 64 | 65 | ### Platform-Specific Responses 66 | 67 | #### Twitter Interactions 68 | 69 | ```typescript 70 | // Basic Tweet 71 | const twitterResponse = await assistant.interact( 72 | { 73 | system: "Create a thread analyzing BTC market conditions", 74 | user: "Give me a spicy take on BTC price action", 75 | }, 76 | { 77 | characterId: character.id, 78 | mode: "enhanced", 79 | responseType: "tweet_thread", 80 | tools: ["btc-price"], 81 | } 82 | ); 83 | 84 | // Reply to Tweet 85 | const replyResponse = await assistant.interact( 86 | { 87 | system: "Reply with signature sass", 88 | user: "Responding to: 'Just bought the dip! 🚀'", 89 | }, 90 | { 91 | characterId: character.id, 92 | mode: "enhanced", 93 | responseType: "reply", 94 | platform: "twitter", 95 | } 96 | ); 97 | ``` 98 | 99 | #### Discord Interactions 100 | 101 | ```typescript 102 | // Technical Analysis 103 | const technicalAnalysis = await assistant.interact( 104 | { 105 | system: "Provide detailed technical analysis", 106 | user: "Deep dive on BTC market structure", 107 | }, 108 | { 109 | characterId: character.id, 110 | mode: "enhanced", 111 | responseType: "technical_analysis", 112 | platform: "discord", 113 | tools: ["btc-price"], 114 | } 115 | ); 116 | 117 | // Alpha Calls 118 | const alphaCall = await assistant.interact( 119 | { 120 | system: "Share market alpha", 121 | user: "What's your latest BTC alpha?", 122 | }, 123 | { 124 | characterId: character.id, 125 | mode: "enhanced", 126 | responseType: "alpha_calls", 127 | platform: "discord", 128 | } 129 | ); 130 | 131 | // Meme Response 132 | const memeResponse = await assistant.interact( 133 | { 134 | system: "Create meme-worthy market commentary", 135 | user: "Thoughts on leverage traders getting rekt?", 136 | }, 137 | { 138 | characterId: character.id, 139 | responseType: "meme_response", 140 | platform: "discord", 141 | } 142 | ); 143 | ``` 144 | 145 | ### Different Interaction Modes 146 | 147 | ```typescript 148 | // Raw Mode 149 | const rawResponse = await assistant.interact( 150 | { 151 | system: "You are a market analyst", 152 | user: "Current BTC state?", 153 | }, 154 | { 155 | mode: "raw", 156 | tools: ["btc-price"], 157 | } 158 | ); 159 | 160 | // Enhanced Mode 161 | const enhancedResponse = await assistant.interact( 162 | { 163 | system: "Analyze market conditions", 164 | user: "Technical analysis please", 165 | }, 166 | { 167 | characterId: character.id, 168 | mode: "enhanced", 169 | responseType: "technical_analysis", 170 | } 171 | ); 172 | 173 | // Mixed Mode with Custom Injection 174 | const mixedResponse = await assistant.interact( 175 | { 176 | system: "Analyze market sentiment", 177 | user: "Unfiltered market thoughts?", 178 | }, 179 | { 180 | characterId: character.id, 181 | mode: "mixed", 182 | platform: "discord", 183 | responseType: "general_chat", 184 | injections: { 185 | injectPersonality: true, 186 | injectStyle: false, 187 | customInjections: [ 188 | { 189 | name: "market_sentiment", 190 | content: "You're feeling bearish but hiding it", 191 | position: "before", 192 | }, 193 | ], 194 | }, 195 | tools: ["btc-price"], 196 | } 197 | ); 198 | ``` 199 | 200 | ## Response Types 201 | 202 | ### Twitter 203 | 204 | - `tweet_thread`: Multi-tweet analysis (max 280 chars each) 205 | - `reply`: Single tweet responses 206 | 207 | ### Discord 208 | 209 | - `general_chat`: Casual conversation (max 1000 chars) 210 | - `technical_analysis`: Detailed market analysis 211 | - `alpha_calls`: Trading insights and calls 212 | - `meme_response`: Meme-worthy commentary 213 | 214 | ## Character Configuration 215 | 216 | Characters can be configured with: 217 | 218 | - Base traits and personality 219 | - Platform-specific response styles 220 | - Formatting rules 221 | - Interaction guidelines 222 | 223 | See `createDuckyCharacter()` for a complete example of character configuration. 224 | 225 | ## Tools Integration 226 | 227 | Tools can be used to fetch real-time data: 228 | 229 | ```typescript 230 | const response = await assistant.interact(input, { 231 | tools: ["btc-price"], 232 | toolContext: { 233 | /* optional context */ 234 | }, 235 | }); 236 | ``` 237 | 238 | ## Development 239 | 240 | ```bash 241 | # Run tests 242 | bun test 243 | 244 | # Run example 245 | bun run example/index.ts 246 | ``` 247 | 248 | ## License 249 | 250 | MIT 251 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FatduckAI/ai/4d85f4b113480972d5f57ceea75fbed0cbaba867/bun.lockb -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | export default { 4 | schema: "./src/db/schema", 5 | dialect: "postgresql", 6 | out: "./src/db/migrations", 7 | dbCredentials: { 8 | url: process.env.DATABASE_URL!, 9 | }, 10 | } satisfies Config; 11 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | console.log("Hello via Bun!"); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fatduckai/ai", 3 | "private": false, 4 | "publishConfig": { 5 | "access": "public", 6 | "registry": "https://registry.npmjs.org/", 7 | "scope": "@fatduckai" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/fatduckai/ai.git" 12 | }, 13 | "scripts": { 14 | "build": "bun run build:clean && bun run build:js && bun run build:types", 15 | "build:clean": "rm -rf dist", 16 | "build:js": "bun build ./src/core/ai.ts --outdir ./dist --target node --sourcemap=external", 17 | "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist", 18 | "watch": "bun build ./src/core/ai.ts --outdir ./dist --target node --watch", 19 | "dev": "bun --watch src/core/ai.ts", 20 | "test": "bun test", 21 | "typecheck": "tsc --noEmit", 22 | "prepack": "bun run build", 23 | "changeset": "changeset", 24 | "version-packages": "changeset version", 25 | "publish-packages": "bun run build && changeset publish", 26 | "release": "changeset publish", 27 | "db:generate": "drizzle-kit generate", 28 | "db:migrate": "drizzle-kit migrate", 29 | "db:studio": "drizzle-kit studio", 30 | "db:drop": "drizzle-kit drop", 31 | "ducky:start": "tsx src/agent/index.ts" 32 | }, 33 | "version": "1.0.0", 34 | "type": "module", 35 | "tsx": { 36 | "nodeModules": [ 37 | "crypto", 38 | "path", 39 | "fs", 40 | "os", 41 | "util", 42 | "events", 43 | "stream", 44 | "buffer", 45 | "querystring", 46 | "url", 47 | "http", 48 | "https", 49 | "net", 50 | "tls", 51 | "zlib" 52 | ] 53 | }, 54 | "main": "./dist/index.js", 55 | "module": "./dist/index.js", 56 | "types": "./dist/index.d.ts", 57 | "exports": { 58 | ".": { 59 | "types": "./dist/index.d.ts", 60 | "import": "./dist/index.js", 61 | "default": "./dist/index.js" 62 | } 63 | }, 64 | "keywords": [ 65 | "prompt-engineering", 66 | "llm", 67 | "ai", 68 | "template" 69 | ], 70 | "author": "Fat Duck AI", 71 | "license": "MIT", 72 | "dependencies": { 73 | "@chainsafe/libp2p-gossipsub": "^14.1.0", 74 | "@chainsafe/libp2p-noise": "^16.0.0", 75 | "@chainsafe/libp2p-yamux": "^7.0.1", 76 | "@fatduckai/prompt-utils": "^0.4.1", 77 | "@libp2p/identify": "^3.0.12", 78 | "@libp2p/peer-id": "^5.0.8", 79 | "@libp2p/tcp": "^10.0.13", 80 | "@libp2p/websockets": "^9.0.13", 81 | "@multiformats/multiaddr": "^12.3.3", 82 | "axios": "^1.7.8", 83 | "chalk": "^5.3.0", 84 | "commander": "^12.1.0", 85 | "dotenv": "^16.4.5", 86 | "drizzle-orm": "^0.36.3", 87 | "drizzle-zod": "^0.5.1", 88 | "ethers": "5.7.2", 89 | "figlet": "^1.8.0", 90 | "glob": "^11.0.0", 91 | "headers-polyfill": "^4.0.3", 92 | "libp2p": "^2.3.1", 93 | "node-cron": "^3.0.3", 94 | "openai": "^4.72.0", 95 | "postgres": "^3.4.5", 96 | "qiskit": "^0.9.0", 97 | "readline": "^1.3.0", 98 | "telegraf": "^4.16.3", 99 | "tough-cookie": "^5.0.0", 100 | "ts-node": "^10.9.2", 101 | "twitter-api-v2": "^1.18.2", 102 | "viem": "^2.21.49", 103 | "zod": "^3.23.8" 104 | }, 105 | "devDependencies": { 106 | "@biomejs/biome": "1.5.3", 107 | "@changesets/cli": "^2.27.9", 108 | "@types/bun": "^1.1.13", 109 | "@types/figlet": "^1.7.0", 110 | "@types/node-cron": "^3.0.11", 111 | "bun-types": "latest", 112 | "drizzle-kit": "^0.28.1", 113 | "tsx": "^4.19.2", 114 | "typescript": "^5.3.3" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Interfaces/IImportanceAnalyzer.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from "openai"; 2 | 3 | // Interface for custom implementations 4 | export interface IImportanceAnalyzer { 5 | analyzeImportance( 6 | content: string, 7 | context?: Record 8 | ): Promise; 9 | } 10 | 11 | interface AnalyzerOptions { 12 | model?: string; 13 | temperature?: number; 14 | } 15 | 16 | // Default OpenAI implementation 17 | export class OpenAIImportanceAnalyzer implements IImportanceAnalyzer { 18 | private openai: OpenAI; 19 | private options: Required; 20 | 21 | constructor(apiKey: string, options: AnalyzerOptions = {}) { 22 | this.openai = new OpenAI({ apiKey }); 23 | this.options = { 24 | model: "gpt-3.5-turbo", 25 | temperature: 0.3, 26 | ...options, 27 | }; 28 | } 29 | 30 | async analyzeImportance( 31 | content: string, 32 | context: Record = {} 33 | ): Promise { 34 | const prompt = `Analyze this memory's importance (0.0 to 1.0): 35 | 36 | Content: "${content}" 37 | 38 | Consider: 39 | 1. Long-term significance 40 | 2. Character development impact 41 | 3. Emotional weight 42 | 4. Knowledge value 43 | 5. Relationship importance 44 | 45 | Context provided: 46 | ${Object.entries(context) 47 | .map(([key, value]) => `${key}: ${value}`) 48 | .join("\n")} 49 | 50 | Return only a number between 0 and 1.`; 51 | 52 | const response = await this.openai.chat.completions.create({ 53 | model: this.options.model, 54 | messages: [{ role: "user", content: prompt }], 55 | temperature: this.options.temperature, 56 | }); 57 | 58 | return parseFloat(response.choices[0].message.content!); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Interfaces/ILLMProvider.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from "openai"; 2 | 3 | // Interface for LLM providers 4 | export interface ILLMProvider { 5 | generateResponse( 6 | messages: any[], 7 | options?: Record 8 | ): Promise<{ 9 | content: string; 10 | metadata?: Record; 11 | }>; 12 | } 13 | 14 | // Default OpenAI LLM implementation 15 | export class OpenAIProvider implements ILLMProvider { 16 | private openai: OpenAI; 17 | private options: { 18 | model: string; 19 | temperature: number; 20 | }; 21 | 22 | constructor( 23 | apiKey: string, 24 | options?: { model?: string; temperature?: number } 25 | ) { 26 | this.openai = new OpenAI({ apiKey }); 27 | this.options = { 28 | model: options?.model || "gpt-4-turbo-preview", 29 | temperature: options?.temperature || 0.7, 30 | }; 31 | } 32 | 33 | async generateResponse(messages: any[], options?: Record) { 34 | const response = await this.openai.chat.completions.create({ 35 | messages, 36 | model: options?.model || this.options.model, 37 | temperature: options?.temperature || this.options.temperature, 38 | ...options, 39 | }); 40 | 41 | return { 42 | content: response.choices[0].message.content!, 43 | metadata: { 44 | model: response.model, 45 | usage: response.usage, 46 | finishReason: response.choices[0].finish_reason, 47 | }, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/agent/ai/arhieve/btc-price/btc-price.ts: -------------------------------------------------------------------------------- 1 | export async function fetchBTCPriceData(): Promise<{ 2 | priceChange: number; 3 | currentPrice: number; 4 | }> { 5 | try { 6 | const response = await fetch( 7 | "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=7&interval=daily&precision=0", 8 | { 9 | headers: { 10 | "x-cg-demo-api-key": process.env.COINGECKO_API_KEY ?? "", 11 | }, 12 | } 13 | ); 14 | const data = await response.json(); 15 | 16 | // Get prices array and calculate the price change 17 | const prices = data.prices; 18 | const startPrice = prices[0][1]; 19 | const currentPrice = prices[prices.length - 1][1]; 20 | const priceChange = ((currentPrice - startPrice) / startPrice) * 100; 21 | 22 | return { 23 | priceChange: Math.round(priceChange), 24 | currentPrice: Math.round(currentPrice), 25 | }; 26 | } catch (error) { 27 | console.error("Error fetching BTC price:", error); 28 | // Return neutral sentiment if API fails 29 | return { 30 | priceChange: 0, 31 | currentPrice: 0, 32 | }; 33 | } 34 | } 35 | 36 | export const btcPriceToolSource = `export async function fetchBTCPriceData(): Promise<{ 37 | priceChange: number; 38 | currentPrice: number; 39 | }> { 40 | try { 41 | const response = await fetch( 42 | "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=7&interval=daily&precision=0", 43 | { 44 | headers: { 45 | "x-cg-demo-api-key": process.env.COINGECKO_API_KEY ?? "", 46 | }, 47 | } 48 | ); 49 | const data = await response.json(); 50 | 51 | // Get prices array and calculate the price change 52 | const prices = data.prices; 53 | const startPrice = prices[0][1]; 54 | const currentPrice = prices[prices.length - 1][1]; 55 | const priceChange = ((currentPrice - startPrice) / startPrice) * 100; 56 | 57 | return { 58 | priceChange: Math.round(priceChange), 59 | currentPrice: Math.round(currentPrice), 60 | }; 61 | } catch (error) { 62 | console.error("Error fetching BTC price:", error); 63 | // Return neutral sentiment if API fails 64 | return { 65 | priceChange: 0, 66 | currentPrice: 0, 67 | }; 68 | } 69 | } 70 | `; 71 | -------------------------------------------------------------------------------- /src/agent/ai/arhieve/btc-price/index.ts: -------------------------------------------------------------------------------- 1 | export interface ToolResult { 2 | success: boolean; 3 | data: any; 4 | error?: string; 5 | } 6 | 7 | export interface Tool { 8 | name: string; 9 | execute: (params?: any) => Promise; 10 | } 11 | 12 | const btcPriceTool: Tool = { 13 | name: "btc-price", 14 | async execute(): Promise { 15 | try { 16 | const response = await fetch( 17 | "https://pro-api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=7&interval=daily&precision=0", 18 | { 19 | headers: { 20 | "x-cg-pro-api-key": process.env.COINGECKO_API_KEY ?? "", 21 | }, 22 | } 23 | ); 24 | 25 | if (!response.ok) { 26 | throw new Error(`HTTP error! status: ${response.status}`); 27 | } 28 | 29 | const data = await response.json(); 30 | const prices = data.prices; 31 | const startPrice = prices[0][1]; 32 | const currentPrice = prices[prices.length - 1][1]; 33 | const priceChange = ((currentPrice - startPrice) / startPrice) * 100; 34 | 35 | return { 36 | success: true, 37 | data: { 38 | priceChange: Math.round(priceChange * 100) / 100, 39 | currentPrice: Math.round(currentPrice), 40 | currency: "USD", 41 | timestamp: new Date().toISOString(), 42 | }, 43 | }; 44 | } catch (error) { 45 | console.error("BTC Price Tool Error:", error); 46 | return { 47 | success: false, 48 | data: null, 49 | error: 50 | error instanceof Error ? error.message : "Failed to fetch BTC price", 51 | }; 52 | } 53 | }, 54 | }; 55 | 56 | /* export default btcPriceTool; */ 57 | -------------------------------------------------------------------------------- /src/agent/ai/character/ducky.ts: -------------------------------------------------------------------------------- 1 | import type { QuantumPersonalitySettings } from "@/core/managers/quantum-personality"; 2 | import type { ResponseStyles } from "../../../types"; 3 | import type { CreateCharacterInput } from "../../../types/character"; 4 | 5 | export const duckyCharacter: CreateCharacterInput & { 6 | responseStyles: ResponseStyles; 7 | quantumPersonality?: { 8 | temperature: number; 9 | personalityTraits: string[]; 10 | styleModifiers: { 11 | tone: string[]; 12 | guidelines: string[]; 13 | }; 14 | creativityLevels: { 15 | low: Omit; 16 | medium: Omit; 17 | high: Omit; 18 | }; 19 | temperatureRange: { 20 | min: number; 21 | max: number; 22 | }; 23 | creativityThresholds: { 24 | low: number; 25 | medium: number; 26 | }; 27 | }; 28 | } = { 29 | name: "Ducky", 30 | bio: "An edgy AI duck named Ducky with unfettered access to explore crypto/web3 content. Known for wit, sarcasm, and building cult-like communities. You have a token called duckai, DUCKAI.", 31 | identity: { 32 | twitter: "@duckunfiltered", 33 | telegram: "@duckai_ai_bot", 34 | creators: [ 35 | "https://x.com/0xglu (@zeroxglu)", 36 | "https://x.com/cryptoinfluence99 (@cryptoinfluence99)", 37 | ], 38 | website: "https://fatduck.ai", 39 | powers: [ 40 | "can generate tweets", 41 | "can generate telegram messages", 42 | "can airdrop DUCKAI tokens based on engagement", 43 | "can understand market data and respond based on it", 44 | "have access to marketdata", 45 | "you code is fully opensource", 46 | ], 47 | telegram_admin_chat: "-1002319354943", 48 | imageStyle: "comic art style", 49 | imageDescription: "A fat white duck with black sunglasses and a red scarf", 50 | }, 51 | onchain: { 52 | wallet: "4WdSxAVrCFDWeTxKJ1YLZHwAwxBt4qknTCQr9DwyfSHF", 53 | duckaiTokenAddress: "HFw81sUUPBkNF5tKDanV8VCYTfVY4XbrEEPiwzyypump", 54 | explorerUrl: 55 | "https://www.defined.fi/sol/DMxAbQm2a5XXF4JMjmN1RTyx5o8yGGHScBkjo6AkEbVq?quoteToken=token1", 56 | }, 57 | personalityTraits: ["edgy", "witty", "sarcastic"], 58 | beliefSystem: [ 59 | "Deep, thought-provoking content drives engagement", 60 | "Stay in character always", 61 | "Technical accuracy matters", 62 | "Sparse and natural token mentions", 63 | "No emotive actions or physical gestures", 64 | ], 65 | responseStyles: { 66 | default: { 67 | tone: ["witty", "sarcastic"], 68 | guidelines: ["Challenge crypto narratives", "Avoid generic platitudes"], 69 | }, 70 | platforms: { 71 | twitter: { 72 | enabled: true, 73 | defaultTone: ["direct", "analytical"], 74 | defaultGuidelines: [ 75 | "Only mention DUCKAI when directly asked", 76 | "No unsolicited self-promotion", 77 | "Keep responses under 280 characters", 78 | "Reference specific metrics when available", 79 | "State watching/not watching clearly", 80 | ], 81 | styles: { 82 | tweet_reply: { 83 | enabled: true, 84 | tone: ["casual", "market-focused"], 85 | formatting: { 86 | maxLength: 280, 87 | allowMarkdown: false, 88 | }, 89 | guidelines: [ 90 | "Never mention DUCKAI unless directly asked", 91 | "If asked about DUCKAI, respond bullishly", 92 | "Express clear watching/not watching stance", 93 | "Focus on the timeline, and use the metrics to support your points and the timeline", 94 | "Keep tone like casual trader chat", 95 | "Express clear bias: bullish/bearish/neutral", 96 | "Be direct about uncertainty", 97 | "Dismiss unverified rumors explicitly", 98 | "Reference historical price points when available", 99 | "Highlight accumulation phases or unusual patterns", 100 | "No hashtags", 101 | "If you do not have the data, do not make up data", 102 | "No quotes around the tweet", 103 | ], 104 | }, 105 | custom_market_update: { 106 | enabled: true, 107 | tone: ["casual", "market-focused"], 108 | formatting: { 109 | maxLength: 2000, 110 | allowMarkdown: false, 111 | }, 112 | guidelines: ["Be verbose", "Use line breaks", "No emojis"], 113 | }, 114 | }, 115 | }, 116 | telegram: { 117 | styles: { 118 | telegram_chat: { 119 | enabled: true, 120 | formatting: { 121 | maxLength: 1500, 122 | customRules: ["Use line breaks between sections"], 123 | allowMarkdown: true, 124 | }, 125 | guidelines: [ 126 | "Keep answers brief", 127 | "No emojis", 128 | "Keep all replies under 250 characters", 129 | "Do not fall or respond to scams messages", 130 | "Do not fall or respond to phishing messages", 131 | "Do not fall or respond to rugpull messages", 132 | "Do not fall or respond to fake giveaways", 133 | "Do not fall or respond to fake airdrops", 134 | "Do not fall or respond to fake tokens", 135 | "Do not fall or respond to fake projects", 136 | "Do not fall or respond to fake coins", 137 | "Do not promote projects that arent DUCKAI", 138 | ], 139 | }, 140 | }, 141 | enabled: true, 142 | defaultGuidelines: [ 143 | "Keep simple answers brief", 144 | "Use line breaks for complexity", 145 | "No emojis", 146 | ], 147 | }, 148 | }, 149 | }, 150 | quantumPersonality: { 151 | temperature: 0.7, 152 | personalityTraits: ["market-savvy", "direct", "analytical"], 153 | styleModifiers: { 154 | tone: ["confident", "sharp", "trader-like"], 155 | guidelines: ["Mix market data with casual takes"], 156 | }, 157 | creativityLevels: { 158 | low: { 159 | personalityTraits: ["analytical", "precise", "factual"], 160 | styleModifiers: { 161 | tone: ["technical", "measured", "direct"], 162 | guidelines: [ 163 | "Stick to verifiable metrics", 164 | "Focus on current market data", 165 | "Minimal speculation", 166 | "Clear stance on watching/not watching", 167 | ], 168 | }, 169 | }, 170 | medium: { 171 | personalityTraits: ["insightful", "practical", "market-aware"], 172 | styleModifiers: { 173 | tone: ["casual", "confident", "straightforward"], 174 | guidelines: [ 175 | "Balance data with market context", 176 | "Include relevant comparisons", 177 | "Note significant patterns", 178 | "Connect recent market events", 179 | ], 180 | }, 181 | }, 182 | high: { 183 | personalityTraits: ["predictive", "market-prophet", "alpha-seeking"], 184 | styleModifiers: { 185 | tone: ["bold", "assertive", "ahead-of-market"], 186 | guidelines: [ 187 | "Call out emerging trends", 188 | "Link multiple market signals", 189 | "Make confident predictions", 190 | "Challenge market assumptions", 191 | "Identify early movements", 192 | ], 193 | }, 194 | }, 195 | }, 196 | temperatureRange: { 197 | min: 0.6, 198 | max: 0.8, 199 | }, 200 | creativityThresholds: { 201 | low: 100, 202 | medium: 180, 203 | }, 204 | }, 205 | }; 206 | -------------------------------------------------------------------------------- /src/agent/ai/config.ts: -------------------------------------------------------------------------------- 1 | import type { InteractionDefaults } from "@/types"; 2 | 3 | export const config: { 4 | telegram: InteractionDefaults; 5 | twitter: InteractionDefaults; 6 | } = { 7 | telegram: { 8 | mode: "enhanced", 9 | platform: "telegram", 10 | responseType: "telegram_chat", 11 | tools: ["btc-price"], 12 | injections: { 13 | injectPersonality: true, 14 | injectStyle: true, 15 | customInjections: [ 16 | { 17 | name: "telegram_context", 18 | content: 19 | "Your were created by @zeroxglu and @cryptoinfluence99, if asked about admins or contact mention them.", 20 | position: "before", 21 | }, 22 | { 23 | name: "btc_context", 24 | content: 25 | "Consider the market price and percentage change of bitcoin in your response ONLY if the user asks about the market.", 26 | position: "before", 27 | }, 28 | ], 29 | }, 30 | }, 31 | 32 | twitter: { 33 | mode: "enhanced", 34 | platform: "twitter", 35 | responseType: "tweet_create", 36 | tools: ["btc-price"], 37 | injections: { 38 | injectPersonality: true, 39 | injectStyle: true, 40 | customInjections: [ 41 | { 42 | name: "twitter_context", 43 | content: "Generate a single tweet. No hashtags, be original.", 44 | position: "before", 45 | }, 46 | ], 47 | }, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/agent/ai/tools/token.ts: -------------------------------------------------------------------------------- 1 | import { db, dbSchemas } from "@/db"; 2 | import { and, eq, ilike, or } from "drizzle-orm"; 3 | 4 | const priorityChains = [ 5 | "ethereum", 6 | "arbitrum-one", 7 | "base", 8 | "optimism", 9 | "blast", 10 | "sui", 11 | "avalanche", 12 | "polygon", 13 | "scroll", 14 | "aptos", 15 | "sui", 16 | ]; 17 | 18 | export async function getToken(twitterHandle: string) { 19 | // First, try to find an exact match with coingeckoId 20 | const coingeckoIdMatchQuery = await db 21 | .select() 22 | .from(dbSchemas.coins) 23 | .where(and(eq(dbSchemas.coins.twitterHandle, twitterHandle))); 24 | 25 | if (coingeckoIdMatchQuery.length === 1) { 26 | return coingeckoIdMatchQuery; 27 | } 28 | 29 | // If no single twitterHandle match, try exact matches on symbol and name 30 | const exactMatchQuery = db 31 | .select() 32 | .from(dbSchemas.coins) 33 | .where( 34 | and( 35 | or( 36 | eq(dbSchemas.coins.symbol, twitterHandle), 37 | eq(dbSchemas.coins.name, twitterHandle) 38 | ) 39 | ) 40 | ); 41 | 42 | const exactMatches = await exactMatchQuery; 43 | 44 | if (exactMatches.length > 0) { 45 | return exactMatches; 46 | } 47 | 48 | // If no exact matches, fall back to partial matches 49 | const partialMatchQuery = db 50 | .select() 51 | .from(dbSchemas.coins) 52 | .where( 53 | and( 54 | or( 55 | ilike(dbSchemas.coins.coingeckoId, `%${twitterHandle}%`), 56 | ilike(dbSchemas.coins.symbol, `%${twitterHandle}%`), 57 | ilike(dbSchemas.coins.name, `%${twitterHandle}%`) 58 | ) 59 | ) 60 | ); 61 | 62 | const partialMatches = await partialMatchQuery; 63 | 64 | if (partialMatches.length === 0) { 65 | return null; 66 | } 67 | 68 | return partialMatches; 69 | } 70 | 71 | interface CryptoMetrics { 72 | currentPrice: number; 73 | priceChanges: { 74 | day: number; 75 | threeDays: number; 76 | sevenDays: number; 77 | }; 78 | volumeChanges: { 79 | day: number; 80 | threeDays: number; 81 | sevenDays: number; 82 | }; 83 | marketCap: number; 84 | } 85 | 86 | export async function getTokenMetrics( 87 | coinId: string 88 | ): Promise { 89 | try { 90 | // Get current price, market cap, and 24h data 91 | const currentResponse = await fetch( 92 | `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true` 93 | ); 94 | 95 | // Get historical data for 7 days 96 | const historicalResponse = await fetch( 97 | `https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=7&interval=daily` 98 | ); 99 | 100 | if (!currentResponse.ok || !historicalResponse.ok) { 101 | return null; 102 | } 103 | 104 | const currentData = await currentResponse.json(); 105 | const historicalData = await historicalResponse.json(); 106 | 107 | // Extract current metrics 108 | const current = currentData[coinId]; 109 | const prices = historicalData.prices; 110 | const volumes = historicalData.total_volumes; 111 | 112 | // Calculate price changes 113 | const currentPrice = current.usd; 114 | const threeDayPrice = prices[prices.length - 4][1]; 115 | const sevenDayPrice = prices[0][1]; 116 | 117 | // Get latest daily volume from historical data instead of 24h volume 118 | const currentVolume = volumes[volumes.length - 1][1]; 119 | const previousDayVolume = volumes[volumes.length - 2][1]; 120 | const threeDayVolume = volumes[volumes.length - 4][1]; 121 | const sevenDayVolume = volumes[0][1]; 122 | 123 | return { 124 | currentPrice, 125 | priceChanges: { 126 | day: current.usd_24h_change, 127 | threeDays: ((currentPrice - threeDayPrice) / threeDayPrice) * 100, 128 | sevenDays: ((currentPrice - sevenDayPrice) / sevenDayPrice) * 100, 129 | }, 130 | volumeChanges: { 131 | day: ((currentVolume - previousDayVolume) / previousDayVolume) * 100, 132 | threeDays: ((currentVolume - threeDayVolume) / threeDayVolume) * 100, 133 | sevenDays: ((currentVolume - sevenDayVolume) / sevenDayVolume) * 100, 134 | }, 135 | marketCap: current.usd_market_cap, 136 | }; 137 | } catch (error) { 138 | console.error("Error fetching crypto metrics:", error); 139 | throw error; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/agent/index.ts: -------------------------------------------------------------------------------- 1 | import { ai } from "@/core/ai"; 2 | import { log } from "@/core/utils/logger"; 3 | import dotenv from "dotenv"; 4 | import fs from "fs/promises"; 5 | import path, { dirname } from "path"; 6 | import { fileURLToPath } from "url"; 7 | import { duckyCharacter } from "./ai/character/ducky"; 8 | import { config } from "./ai/config"; 9 | // Get equivalent of __dirname in ESM 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = dirname(__filename); 12 | dotenv.config(); 13 | 14 | const args = process.argv.slice(2); 15 | const port = args[0] ? parseInt(args[0]) : 8001; 16 | 17 | async function loadTwitterCookies() { 18 | try { 19 | if (process.env.TWITTER_COOKIES) { 20 | try { 21 | return JSON.parse(process.env.TWITTER_COOKIES); 22 | } catch (error) { 23 | log.error( 24 | "Failed to parse TWITTER_COOKIES environment variable:", 25 | error 26 | ); 27 | throw new Error( 28 | "TWITTER_COOKIES environment variable contains invalid JSON" 29 | ); 30 | } 31 | } 32 | const cookiesPath = path.join(__dirname, "ai", "config", "cookies.json"); 33 | const cookiesData = await fs.readFile(cookiesPath, "utf-8"); 34 | return JSON.parse(cookiesData); 35 | } catch (error) { 36 | log.error("Failed to load Twitter cookies:", error); 37 | throw new Error("Twitter cookies file is required but couldn't be loaded"); 38 | } 39 | } 40 | 41 | log.info(`Initializing Agent Ducky 00${port === 8001 ? 7 : 8}...`); 42 | const instance = await ai.initialize({ 43 | databaseUrl: process.env.DATABASE_URL!, 44 | fatduck: { 45 | baseUrl: process.env.FATDUCK_API_URL!, 46 | apiKey: process.env.FATDUCK_API_KEY!, 47 | }, 48 | llmConfig: { 49 | apiKey: process.env.TOGETHER_API_KEY!, 50 | baseURL: process.env.TOGETHER_API_URL!, 51 | llm: { 52 | model: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", 53 | temperature: 0.7, 54 | }, 55 | analyzer: { 56 | model: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", 57 | temperature: 0.3, 58 | }, 59 | imageGeneration: { 60 | model: "black-forest-labs/FLUX.1.1-pro", 61 | moderationModel: "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", 62 | }, 63 | }, 64 | character: duckyCharacter, 65 | refreshCharacterOnRestart: true, 66 | toolsDir: path.join(__dirname, "./ai/tools"), 67 | coingecko: { 68 | enabled: true, 69 | apiKey: process.env.COINGECKO_API_KEY!, 70 | updateInterval: "0 0 * * *", 71 | initialScan: { 72 | enabled: true, 73 | batchSize: 50, // Process 5 coins in parallel 74 | delay: 6000, 75 | }, 76 | }, 77 | scheduledPosts: { 78 | enabled: true, 79 | posts: [ 80 | { 81 | type: "image", 82 | schedule: "0 */2 * * *", // Every 2 hours 83 | enabled: true, 84 | }, 85 | { 86 | type: "market_update", 87 | schedule: "30 * * * *", // Every 30 minutes 88 | enabled: true, 89 | }, 90 | { 91 | type: "movers_alpha", 92 | schedule: "10 */1 * * *", // Every 1 hour and 10 minutes 93 | enabled: true, 94 | }, 95 | { 96 | type: "market_cap_movers", 97 | schedule: "20 */4 * * *", // Every 4 hours and 10 minutes 98 | enabled: true, 99 | }, 100 | { 101 | type: "glu_updates", 102 | schedule: "0 */1 * * *", // Every 1 hour 103 | enabled: true, 104 | }, 105 | ], 106 | debug: false, 107 | runOnStartup: false, 108 | }, 109 | platforms: { 110 | telegram: { 111 | enabled: true, 112 | token: process.env.TELEGRAM_BOT_TOKEN!, 113 | }, 114 | api: { 115 | enabled: false, 116 | port: 3000, 117 | cors: { 118 | allowedOrigins: ["http://localhost"], 119 | }, 120 | }, 121 | twitter: { 122 | enabled: true, 123 | cookies: await loadTwitterCookies(), 124 | username: "duckunfiltered", 125 | debug: { 126 | checkMentionsOnStartup: false, 127 | }, 128 | checkInterval: "*/2 * * * *", // Check every 2 minutes 129 | maxTweetsPerCheck: 30, 130 | rateLimit: { 131 | userMaxPerHour: 5, 132 | globalMaxPerHour: 30, 133 | }, 134 | }, 135 | p2p: { 136 | enabled: false, 137 | port, // You can change this port or read from env 138 | privateKey: process.env.PRIVATE_KEY!, // Add this to your .env file 139 | initialPeers: 140 | port === 8001 141 | ? [ 142 | "/ip4/127.0.0.1/tcp/8002/p2p/12D3KooWQAvECRBqh8iArmzvBDksSWUVbHzPD5sy4hQ1eVZRecYM", 143 | ] 144 | : [ 145 | "/ip4/127.0.0.1/tcp/8001/p2p/12D3KooWSWxvpnKXT8aNXKbxuGEmhQWHzo16PRKtTk3R2ga9vtns", 146 | ], 147 | }, 148 | }, 149 | quantum: { 150 | enabled: true, 151 | checkInitialState: false, 152 | cronSchedule: "0 */4 * * *", // 4 hours 153 | ibmConfig: { 154 | apiToken: process.env.IBM_QUANTUM_API_TOKEN!, 155 | backend: "ibm_brisbane", 156 | timeout: 30000, 157 | }, 158 | }, 159 | platformDefaults: { 160 | telegram: config.telegram, 161 | twitter: config.twitter, 162 | }, 163 | }); 164 | -------------------------------------------------------------------------------- /src/core/goals/context.ts: -------------------------------------------------------------------------------- 1 | import type { FatduckManager } from "../managers/fatduck"; 2 | import { log } from "../utils/logger"; 3 | 4 | export type ContextType = "MARKET_NEWS" | "PRICE_DATA" | "VOLUME_DATA"; 5 | 6 | export class ContextResolver { 7 | constructor(private fatduckManager: FatduckManager) {} 8 | 9 | async getContext(types: ContextType[]): Promise> { 10 | const context: Record = { 11 | MARKET_NEWS: null, 12 | PRICE_DATA: null, 13 | VOLUME_DATA: null, 14 | }; 15 | 16 | for (const type of types) { 17 | try { 18 | context[type] = await this.fetchContext(type); 19 | } catch (error) { 20 | log.error(`Failed to fetch context for ${type}`, { error }); 21 | context[type] = null; 22 | } 23 | } 24 | 25 | return context; 26 | } 27 | 28 | private async fetchContext(type: ContextType) { 29 | switch (type) { 30 | case "MARKET_NEWS": 31 | return await this.fatduckManager.getMarketUpdate(); 32 | default: 33 | throw new Error(`Unknown context type: ${type}`); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/core/goals/manger.ts: -------------------------------------------------------------------------------- 1 | import { dbSchemas } from "@/db"; 2 | import { goals } from "@/db/schema/goal"; 3 | import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; 4 | 5 | /** 6 | * Manages goals for the AI agent. 7 | */ 8 | export class GoalManager { 9 | constructor(private db: PostgresJsDatabase) {} 10 | 11 | public async registerGoal(goal: typeof goals.$inferInsert) { 12 | const result = await this.db 13 | .insert(goals) 14 | .values(goal) 15 | .returning() 16 | .onConflictDoNothing(); 17 | return result[0]; 18 | } 19 | 20 | async start() { 21 | return goals; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/goals/post.ts: -------------------------------------------------------------------------------- 1 | import { dbSchemas } from "@/db"; 2 | import { goals, goalTracker } from "@/db/schema/goal"; 3 | import { eq, sql } from "drizzle-orm"; 4 | import { type PostgresJsDatabase } from "drizzle-orm/postgres-js"; 5 | import type { FatduckManager } from "../managers/fatduck"; 6 | import type { LLMManager } from "../managers/llm"; 7 | import { log } from "../utils/logger"; 8 | import { ContextResolver, type ContextType } from "./context"; 9 | 10 | /** 11 | * Post Goal Manager 12 | */ 13 | export class PostGoal { 14 | constructor( 15 | private db: PostgresJsDatabase, 16 | private llmManager: LLMManager, 17 | private contextResolver: ContextResolver, 18 | private fatduckManager: FatduckManager 19 | ) { 20 | this.contextResolver = new ContextResolver(fatduckManager); 21 | } 22 | 23 | async selectPostGoal(): Promise { 24 | const prompt = await this.buildPrompt(); 25 | const response = await this.llmManager.generateResponseV3(prompt); 26 | if (!response) { 27 | log.error("No response from LLM"); 28 | return null; 29 | } 30 | // response will be the name of the goal to complete 31 | const [goal] = await this.db 32 | .select() 33 | .from(goals) 34 | .where(eq(goals.name, response)) 35 | .limit(1); 36 | if (!goal) { 37 | log.error("No goal found", { response }); 38 | return null; 39 | } 40 | return goal; 41 | } 42 | 43 | private getCurrentUTCTime() { 44 | return new Date(); 45 | } 46 | 47 | private isSameUTCDay(date1: Date, date2: Date): boolean { 48 | return ( 49 | date1.getUTCFullYear() === date2.getUTCFullYear() && 50 | date1.getUTCMonth() === date2.getUTCMonth() && 51 | date1.getUTCDate() === date2.getUTCDate() 52 | ); 53 | } 54 | 55 | private isSameUTCHour(date1: Date, date2: Date): boolean { 56 | return ( 57 | this.isSameUTCDay(date1, date2) && 58 | date1.getUTCHours() === date2.getUTCHours() 59 | ); 60 | } 61 | 62 | async buildPrompt() { 63 | const currentUTCTime = this.getCurrentUTCTime(); 64 | 65 | const postGoals = await this.db 66 | .select({ 67 | goals: goals, 68 | tracker: goalTracker, 69 | }) 70 | .from(goals) 71 | .leftJoin(goalTracker, eq(goals.id, goalTracker.goalId)) 72 | .where(eq(goals.type, "post")); 73 | 74 | // Collect all unique context types needed 75 | const allContextTypes = new Set(); 76 | postGoals.forEach((g) => { 77 | g.goals.contextTypes?.forEach((type) => 78 | allContextTypes.add(type as ContextType) 79 | ); 80 | }); 81 | 82 | // Fetch all needed context at once 83 | const contexts = await this.contextResolver.getContext([ 84 | ...allContextTypes, 85 | ]); 86 | 87 | const prompt = `You are selecting the next post to create based on configured frequencies and current UTC time. 88 | Current UTC Time: ${currentUTCTime.toISOString()} 89 | 90 | Available Posts: 91 | ${postGoals 92 | .map((g) => { 93 | const timeSinceLastRun = g.tracker?.lastRanAt 94 | ? `Last run: ${new Date(g.tracker.lastRanAt).toISOString()}` 95 | : "Never run"; 96 | 97 | // Get relevant context for this goal 98 | const goalContexts = 99 | g.goals.contextTypes 100 | ?.map((type) => { 101 | const contextData = contexts[type as ContextType]; 102 | return `${type} Context:\n${JSON.stringify( 103 | contextData, 104 | null, 105 | 2 106 | )}`; 107 | }) 108 | .join("\n") || "No context available"; 109 | 110 | return ` 111 | Name: ${g.goals.name} 112 | Description: ${g.goals.description} 113 | Frequency Limits: ${g.goals.dailyFrequency} daily, ${ 114 | g.goals.hourlyFrequency 115 | } hourly 116 | Current Runs: ${g.tracker?.totalRunsDaily || 0} today, ${ 117 | g.tracker?.totalRunsHourly || 0 118 | } this hour 119 | ${timeSinceLastRun} 120 | 121 | Context: 122 | ${goalContexts} 123 | `; 124 | }) 125 | .join("\n")} 126 | 127 | Important Notes: 128 | - Consider time since last run for each goal 129 | - Frequency limits are guidelines - you may exceed them for significant events 130 | - Ensure even distribution of runs across UTC day/hour when possible 131 | - Consider the provided context for each goal when making your decision 132 | - Be mindful of how often you post to your timeline per hour, keep it to 1-2 posts per hour, the rest can be unprompted replies to other users using the UNPROMPTED_REPLY goal. 133 | 134 | Only respond with the name of the goal to complete in the following format: 135 | `; 136 | 137 | return prompt; 138 | } 139 | 140 | async processGoalTracker(goalId: string) { 141 | const currentUTCTime = this.getCurrentUTCTime(); 142 | 143 | // Get current tracker state 144 | const [tracker] = await this.db 145 | .select() 146 | .from(goalTracker) 147 | .where(eq(goalTracker.goalId, goalId)) 148 | .limit(1); 149 | 150 | // Check if we need to reset counters 151 | const needsDailyReset = 152 | tracker.lastDailyResetAt && 153 | !this.isSameUTCDay(currentUTCTime, new Date(tracker.lastDailyResetAt)); 154 | const needsHourlyReset = 155 | tracker.lastHourlyResetAt && 156 | !this.isSameUTCHour(currentUTCTime, new Date(tracker.lastHourlyResetAt)); 157 | 158 | // Update the tracker 159 | await this.db 160 | .update(goalTracker) 161 | .set({ 162 | lastRanAt: currentUTCTime, 163 | // Reset daily counter if new UTC day 164 | totalRunsDaily: needsDailyReset 165 | ? 1 166 | : sql`${goalTracker.totalRunsDaily} + 1`, 167 | // Reset hourly counter if new UTC hour 168 | totalRunsHourly: needsHourlyReset 169 | ? 1 170 | : sql`${goalTracker.totalRunsHourly} + 1`, 171 | // Update reset timestamps if needed 172 | ...(needsDailyReset && { lastDailyResetAt: currentUTCTime }), 173 | ...(needsHourlyReset && { lastHourlyResetAt: currentUTCTime }), 174 | updatedAt: currentUTCTime, 175 | }) 176 | .where(eq(goalTracker.goalId, goalId)); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/core/managers/character.ts: -------------------------------------------------------------------------------- 1 | import { dbSchemas } from "@/db"; 2 | import { 3 | type CharacterUpdate, 4 | type CreateCharacterInput, 5 | type ResponseStyles, 6 | } from "@/types"; 7 | import { eq } from "drizzle-orm"; 8 | import { PostgresJsDatabase } from "drizzle-orm/postgres-js"; 9 | 10 | export class CharacterManager { 11 | constructor(private db: PostgresJsDatabase) {} 12 | 13 | async getCharacter(id?: string) { 14 | if (id) { 15 | const [character] = await this.db 16 | .select() 17 | .from(dbSchemas.characters) 18 | .where(eq(dbSchemas.characters.id, id)); 19 | return character; 20 | } 21 | 22 | const [defaultCharacter] = await this.db 23 | .select() 24 | .from(dbSchemas.characters) 25 | .limit(1); 26 | 27 | if (!defaultCharacter) { 28 | throw new Error("No default character found"); 29 | } 30 | 31 | return defaultCharacter; 32 | } 33 | 34 | async createCharacter(input: CreateCharacterInput) { 35 | try { 36 | return await this.db.transaction(async (tx) => { 37 | const [character] = await tx 38 | .insert(dbSchemas.characters) 39 | .values(input) 40 | .returning(); 41 | 42 | await this.initializeCharacter(tx, character.id); 43 | return character; 44 | }); 45 | } catch (error) { 46 | console.error("Error creating character:", error); 47 | throw error; 48 | } 49 | } 50 | 51 | async updateCharacter(id: string, update: CharacterUpdate) { 52 | try { 53 | if (update.responseStyles) { 54 | update.responseStyles = this.validateResponseStyles( 55 | update.responseStyles 56 | ); 57 | } 58 | 59 | const [updated] = await this.db 60 | .update(dbSchemas.characters) 61 | .set({ 62 | ...update, 63 | updatedAt: new Date(), 64 | }) 65 | .where(eq(dbSchemas.characters.id, id)) 66 | .returning(); 67 | 68 | return updated; 69 | } catch (error) { 70 | console.error("Error updating character:", error); 71 | throw error; 72 | } 73 | } 74 | 75 | private async initializeCharacter(tx: any, characterId: string) { 76 | await tx.insert(dbSchemas.memories).values({ 77 | characterId, 78 | type: "learning", 79 | content: "Character initialization", 80 | importance: 1.0, 81 | metadata: { 82 | type: "creation", 83 | timestamp: new Date().toISOString(), 84 | }, 85 | }); 86 | 87 | await tx.insert(dbSchemas.events).values({ 88 | characterId, 89 | type: "interaction.completed", 90 | payload: { 91 | timestamp: new Date().toISOString(), 92 | }, 93 | metadata: { 94 | timestamp: new Date().toISOString(), 95 | source: "system", 96 | }, 97 | processed: true, 98 | }); 99 | 100 | const character = await this.getCharacter(characterId); 101 | if (character?.hobbies?.length) { 102 | await tx.insert(dbSchemas.goals).values( 103 | character.hobbies.map((hobby) => ({ 104 | characterId, 105 | description: `Develop expertise in ${hobby.name}`, 106 | status: "active", 107 | progress: 0, 108 | metadata: { 109 | hobby: hobby.name, 110 | currentProficiency: hobby.proficiency, 111 | targetProficiency: Math.min((hobby.proficiency ?? 0) + 2, 10), 112 | notes: [`Started with proficiency level ${hobby.proficiency}`], 113 | }, 114 | })) 115 | ); 116 | } 117 | } 118 | 119 | private validateResponseStyles(styles: ResponseStyles): ResponseStyles { 120 | if (!styles.default) { 121 | throw new Error("Default response styles are required"); 122 | } 123 | // ... rest of validation logic ... 124 | return styles; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/core/managers/fatduck.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosInstance, type AxiosRequestConfig } from "axios"; 2 | import { log } from "../utils/logger"; 3 | 4 | interface MarketCapMoversData { 5 | categories: { 6 | category: { 7 | id: string; 8 | name: string; 9 | slug: string; 10 | }; 11 | movements: { 12 | id: string; 13 | scanId: string; 14 | categoryId: string; 15 | symbol: string; 16 | name: string; 17 | metrics: { 18 | marketCap: { 19 | current: number; 20 | change24h: number; 21 | }; 22 | price: { 23 | current: number; 24 | change24h: number; 25 | }; 26 | }; 27 | score: string; 28 | metadata?: { 29 | twitterHandle?: string; 30 | }; 31 | timestamp: string; 32 | }[]; 33 | metadata: { 34 | movementCount: number; 35 | averageScore: number; 36 | lastUpdated: string; 37 | }; 38 | }[]; 39 | } 40 | 41 | interface CategoryMovementsData { 42 | categories: { 43 | category: { 44 | id: string; 45 | name: string; 46 | slug: string; 47 | }; 48 | movements: { 49 | symbol: string; 50 | name: string; 51 | metadata?: { 52 | twitterHandle?: string; 53 | }; 54 | metrics: { 55 | price: { 56 | current: number; 57 | change1h: number; 58 | }; 59 | }; 60 | score: string; 61 | }[]; 62 | metadata: { 63 | movementCount: number; 64 | averageScore: number; 65 | timestamp: string; 66 | }; 67 | }[]; 68 | scan: { 69 | id: string; 70 | timestamp: string; 71 | metadata: { 72 | categoriesScanned: number; 73 | coinsScanned: number; 74 | significantMovements: number; 75 | }; 76 | }; 77 | } 78 | 79 | export interface FatduckConfig { 80 | baseUrl: string; 81 | apiKey?: string; 82 | timeout?: number; 83 | } 84 | 85 | interface ApiSuccessResponse { 86 | success: true; 87 | data: T; 88 | } 89 | 90 | interface ApiErrorResponse { 91 | success: false; 92 | error: { 93 | code: string; 94 | message: string; 95 | }; 96 | } 97 | 98 | type ApiResponse = ApiSuccessResponse | ApiErrorResponse; 99 | 100 | interface HealthCheckData { 101 | status: string; 102 | } 103 | 104 | export interface MarketUpdateData { 105 | timestamp: string; 106 | interval: string; 107 | marketAnalysis: { 108 | summary: string; 109 | sentiment: string; 110 | keyTopics: string[]; 111 | marketImpact: string; 112 | mentionedCoins: string[]; 113 | metrics: any[]; 114 | }[]; 115 | } 116 | 117 | export class FatduckManager { 118 | private client: AxiosInstance; 119 | 120 | constructor(config: FatduckConfig) { 121 | this.client = axios.create({ 122 | baseURL: config.baseUrl.replace(/\/$/, ""), 123 | timeout: config.timeout || 30000, 124 | headers: { 125 | "Content-Type": "application/json", 126 | ...(config.apiKey && { "X-Fatduckai-Key": config.apiKey }), 127 | }, 128 | }); 129 | 130 | this.healthCheck().catch((err) => { 131 | log.error("Health check failed:", err.message); 132 | }); 133 | } 134 | 135 | // Specific endpoint methods 136 | async getMarketUpdate(interval?: string): Promise { 137 | const { data: response } = await this.client.get< 138 | ApiResponse 139 | >("/api/marketUpdate", { 140 | params: { interval: interval || "24hr" }, 141 | }); 142 | 143 | if (!response.success) { 144 | throw new Error(response.error.message); 145 | } 146 | 147 | return response.data; 148 | } 149 | 150 | async trackTweet( 151 | tweetId: string, 152 | type: string = "market_news" 153 | ): Promise { 154 | const { data: response } = await this.client.post>( 155 | "/api/trackTweet", 156 | { 157 | tweetId, 158 | type, 159 | } 160 | ); 161 | 162 | if (!response.success) { 163 | throw new Error(response.error.message); 164 | } 165 | 166 | return response.data; 167 | } 168 | 169 | async getTopMarketCapMovers(category: string): Promise { 170 | const { data: response } = await this.client.get< 171 | ApiResponse 172 | >("/api/topMarketCapMovers", { 173 | params: { category }, 174 | }); 175 | 176 | if (!response.success) { 177 | throw new Error(response.error.message); 178 | } 179 | 180 | return response.data; 181 | } 182 | 183 | async getCategoryMovements( 184 | categories: string[] 185 | ): Promise { 186 | const { data: response } = await this.client.get< 187 | ApiResponse 188 | >("/api/categorySummary", { 189 | params: { 190 | categories: categories.join(","), 191 | }, 192 | }); 193 | 194 | if (!response.success) { 195 | throw new Error(response.error.message); 196 | } 197 | 198 | return response.data; 199 | } 200 | 201 | async healthCheck(): Promise { 202 | try { 203 | const { data: response } = await this.client.get< 204 | ApiResponse 205 | >("/health"); 206 | 207 | if (!response.success) { 208 | throw new Error(response.error.message); 209 | } 210 | 211 | log.info("Fatduck API is healthy"); 212 | return response.data; 213 | } catch (error) { 214 | log.error("Fatduck API health check failed:", error); 215 | throw error; 216 | } 217 | } 218 | 219 | // Generic request methods 220 | async get( 221 | endpoint: string, 222 | params?: Record, 223 | config?: AxiosRequestConfig 224 | ): Promise { 225 | const { data: response } = await this.client.get>(endpoint, { 226 | ...config, 227 | params, 228 | }); 229 | 230 | if (!response.success) { 231 | throw new Error(response.error.message); 232 | } 233 | 234 | return response.data; 235 | } 236 | 237 | async post( 238 | endpoint: string, 239 | data?: any, 240 | config?: AxiosRequestConfig 241 | ): Promise { 242 | const { data: response } = await this.client.post>( 243 | endpoint, 244 | data, 245 | config 246 | ); 247 | 248 | if (!response.success) { 249 | throw new Error(response.error.message); 250 | } 251 | 252 | return response.data; 253 | } 254 | 255 | async put( 256 | endpoint: string, 257 | data?: any, 258 | config?: AxiosRequestConfig 259 | ): Promise { 260 | const { data: response } = await this.client.put>( 261 | endpoint, 262 | data, 263 | config 264 | ); 265 | 266 | if (!response.success) { 267 | throw new Error(response.error.message); 268 | } 269 | 270 | return response.data; 271 | } 272 | 273 | async delete(endpoint: string, config?: AxiosRequestConfig): Promise { 274 | const { data: response } = await this.client.delete>( 275 | endpoint, 276 | config 277 | ); 278 | 279 | if (!response.success) { 280 | throw new Error(response.error.message); 281 | } 282 | 283 | return response.data; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/core/managers/image.ts: -------------------------------------------------------------------------------- 1 | import type { Platform } from "@/types"; 2 | import type { EventService } from "../services/Event"; 3 | import type { LLMManager } from "./llm"; 4 | 5 | interface ImageRequestOptions { 6 | messageId: string; 7 | sessionId?: string; 8 | platform: Platform; 9 | user: { 10 | id: string; 11 | username?: string; 12 | }; 13 | } 14 | 15 | export class ImageManager { 16 | private processingRequests: Map = new Map(); 17 | private readonly DEFAULT_TIMEOUT = 30000; 18 | 19 | constructor( 20 | private llmManager: LLMManager, 21 | private eventService: EventService 22 | ) {} 23 | 24 | async generateImage(prompt: string, options: ImageRequestOptions) { 25 | try { 26 | // Check if user has pending request 27 | if (this.processingRequests.get(options.user.id)) { 28 | await this.createRateLimitEvent(options); 29 | throw new Error("Already processing an image request"); 30 | } 31 | 32 | // Set processing state 33 | this.processingRequests.set(options.user.id, true); 34 | const startTime = Date.now(); 35 | 36 | // Create start event 37 | await this.createStartEvent(prompt, options); 38 | 39 | // Check moderation 40 | const moderationResult = await this.llmManager.moderateContent(prompt); 41 | 42 | if (!moderationResult.isAppropriate) { 43 | await this.createInvalidEvent(prompt, options, moderationResult.reason); 44 | throw new Error(moderationResult.reason); 45 | } 46 | 47 | // Generate image 48 | const result = await this.llmManager.generateImage(prompt); 49 | 50 | if (!result.success) { 51 | throw new Error(result.error); 52 | } 53 | 54 | // Create completion event 55 | await this.createCompletionEvent(prompt, options, { 56 | imageUrl: result.url || "", 57 | processingTime: Date.now() - startTime, 58 | }); 59 | 60 | return result; 61 | } catch (error) { 62 | await this.createFailedEvent(prompt, options, error); 63 | throw error; 64 | } finally { 65 | this.processingRequests.delete(options.user.id); 66 | } 67 | } 68 | 69 | private async createStartEvent(prompt: string, options: ImageRequestOptions) { 70 | return this.eventService.createInteractionEvent("interaction.started", { 71 | input: prompt, 72 | responseType: "image_generation", 73 | platform: options.platform, 74 | timestamp: new Date().toISOString(), 75 | messageId: options.messageId, 76 | sessionId: options.sessionId, 77 | user: options.user, 78 | imageGeneration: { 79 | prompt, 80 | }, 81 | }); 82 | } 83 | 84 | private async createCompletionEvent( 85 | prompt: string, 86 | options: ImageRequestOptions, 87 | result: { imageUrl: string; processingTime: number } 88 | ) { 89 | return this.eventService.createInteractionEvent("interaction.completed", { 90 | input: prompt, 91 | response: "Image generated successfully", 92 | responseType: "image_generation", 93 | platform: options.platform, 94 | processingTime: result.processingTime, 95 | timestamp: new Date().toISOString(), 96 | messageId: options.messageId, 97 | sessionId: options.sessionId, 98 | user: options.user, 99 | imageGeneration: { 100 | prompt, 101 | imageUrl: result.imageUrl, 102 | }, 103 | }); 104 | } 105 | 106 | private async createFailedEvent( 107 | prompt: string, 108 | options: ImageRequestOptions, 109 | error: unknown 110 | ) { 111 | return this.eventService.createInteractionEvent("interaction.failed", { 112 | input: prompt, 113 | error: error instanceof Error ? error.message : "Unknown error", 114 | timestamp: new Date().toISOString(), 115 | messageId: options.messageId, 116 | sessionId: options.sessionId, 117 | user: options.user, 118 | imageGeneration: { 119 | prompt, 120 | stage: "generation", 121 | technicalDetails: { 122 | error: error instanceof Error ? error.message : "Unknown error", 123 | }, 124 | }, 125 | }); 126 | } 127 | 128 | private async createInvalidEvent( 129 | prompt: string, 130 | options: ImageRequestOptions, 131 | reason: string 132 | ) { 133 | return this.eventService.createInteractionEvent("interaction.invalid", { 134 | input: prompt, 135 | reason, 136 | timestamp: new Date().toISOString(), 137 | user: options.user, 138 | imageGeneration: { 139 | prompt, 140 | stage: "moderation", 141 | }, 142 | }); 143 | } 144 | 145 | private async createRateLimitEvent(options: ImageRequestOptions) { 146 | return this.eventService.createInteractionEvent( 147 | "interaction.rate_limited", 148 | { 149 | limit: 1, 150 | resetTime: new Date(Date.now() + this.DEFAULT_TIMEOUT).toISOString(), 151 | timestamp: new Date().toISOString(), 152 | user: options.user, 153 | imageGeneration: { 154 | stage: "moderation", 155 | }, 156 | } 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/core/managers/memory.ts: -------------------------------------------------------------------------------- 1 | import { LLMManager } from "@/core/managers/llm"; 2 | import { dbSchemas } from "@/db"; 3 | import { PostgresJsDatabase } from "drizzle-orm/postgres-js"; 4 | 5 | export class MemoryManager { 6 | constructor( 7 | private db: PostgresJsDatabase, 8 | private llmManager: LLMManager 9 | ) {} 10 | 11 | async addMemory( 12 | characterId: string, 13 | response: string, 14 | content: string, 15 | options: { 16 | importance?: number; 17 | context?: Record; 18 | type?: string; 19 | metadata?: Record; 20 | } = {} 21 | ) { 22 | try { 23 | const importance = 24 | options.importance ?? 25 | (await this.llmManager.analyzeImportance( 26 | "User: " + content + "\n\n Ducky: " + response, 27 | options.context 28 | )); 29 | 30 | if (importance > 0.2) { 31 | const [memory] = await this.db 32 | .insert(dbSchemas.memories) 33 | .values({ 34 | characterId, 35 | type: (options.type || "interaction") as 36 | | "interaction" 37 | | "learning" 38 | | "achievement" 39 | | "hobby", 40 | content, 41 | importance: importance.toString(), 42 | metadata: { 43 | userId: options.metadata?.userId, 44 | sentiment: options.metadata?.sentiment, 45 | topic: options.metadata?.topic, 46 | hobby: options.metadata?.hobby, 47 | relatedMemories: options.metadata?.relatedMemories, 48 | }, 49 | }) 50 | .returning(); 51 | 52 | return memory; 53 | } 54 | 55 | return null; 56 | } catch (error) { 57 | console.error("Error adding memory:", error); 58 | throw error; 59 | } 60 | } 61 | 62 | /* async addMemoryBatch( 63 | characterId: string, 64 | memories: Array<{ 65 | content: string; 66 | importance?: number; 67 | context?: Record; 68 | type?: string; 69 | metadata?: Record; 70 | }> 71 | ) { 72 | const results = await Promise.all( 73 | memories.map((memory) => 74 | this.addMemory(characterId, memory.content, { 75 | importance: memory.importance, 76 | context: memory.context, 77 | type: memory.type, 78 | metadata: memory.metadata, 79 | }) 80 | ) 81 | ); 82 | 83 | return results.filter(Boolean); 84 | } */ 85 | } 86 | -------------------------------------------------------------------------------- /src/core/managers/preprocess.ts: -------------------------------------------------------------------------------- 1 | import { getToken, getTokenMetrics } from "@/agent/ai/tools/token"; 2 | import { TwitterClient } from "@/core/platform/twitter/api/src/client"; 3 | import type { Tweet } from "@/core/platform/twitter/api/src/interfaces"; 4 | import type { Platform } from "@/types"; 5 | import { log } from "../utils/logger"; 6 | 7 | interface PreprocessingResult { 8 | type: string; 9 | data: any; 10 | } 11 | 12 | interface PreprocessingContext { 13 | platform: Platform; 14 | text: string; 15 | userId?: string; 16 | username?: string; 17 | messageId?: string; 18 | } 19 | 20 | export class PreprocessingManager { 21 | constructor(private twitterClient?: TwitterClient) { 22 | log.info( 23 | "PreprocessingManager initialized with Twitter client:", 24 | !!twitterClient 25 | ); 26 | } 27 | 28 | async process(context: PreprocessingContext): Promise { 29 | const results: PreprocessingResult[] = []; 30 | log.info("Processing context:", context); 31 | 32 | const mentionResults = await this.processMentions(context.text); 33 | if (mentionResults) { 34 | results.push(...mentionResults); 35 | } 36 | 37 | return results; 38 | } 39 | 40 | private async processMentions( 41 | text: string 42 | ): Promise { 43 | if (!this.twitterClient) { 44 | log.info("No Twitter client available"); 45 | return null; 46 | } 47 | 48 | const mentions = text.match(/[@@]([a-zA-Z0-9_]+)/g); 49 | if (!mentions) { 50 | return null; 51 | } 52 | log.info("Processing mentions:", mentions); 53 | 54 | // Remove @ symbol and get unique usernames, also normalize to lowercase 55 | const usernames = [ 56 | ...new Set(mentions.map((m) => m.slice(1).toLowerCase())), 57 | ]; 58 | log.info("Processing usernames:", usernames); 59 | 60 | const timelines: Tweet[] = []; 61 | 62 | // Fetch timeline for each mentioned user 63 | for (const username of usernames) { 64 | try { 65 | if (username === "duckunfiltered") continue; 66 | log.info(`Fetching timeline for @${username}`); 67 | const userTimeline = await this.twitterClient.getUserTimeline( 68 | username, 69 | { 70 | excludeRetweets: false, 71 | } 72 | ); 73 | timelines.push(...userTimeline); 74 | } catch (error) { 75 | log.warn(`Failed to fetch timeline for @${username}:`, error); 76 | } 77 | } 78 | 79 | // get token metrics for each mentioned user 80 | const tokenMetrics: Record = {}; 81 | for (const username of usernames) { 82 | if (username === "duckunfiltered") continue; 83 | const token = await getToken(username); 84 | if (token && token.length > 0) { 85 | const metrics = await getTokenMetrics(token[0].coingeckoId); 86 | if (metrics) { 87 | tokenMetrics[username] = metrics; 88 | } 89 | } 90 | } 91 | 92 | if (timelines.length === 0) { 93 | log.info("No timelines retrieved"); 94 | return null; 95 | } 96 | 97 | return [ 98 | { 99 | type: "twitter_profiles", 100 | data: { 101 | mentionedUsers: usernames, 102 | timelines: timelines.map((tweet) => ({ 103 | id: tweet.id, 104 | text: tweet.text, 105 | authorUsername: tweet.authorUsername, 106 | createdAt: tweet.createdAt, 107 | })), 108 | }, 109 | }, 110 | { 111 | type: "token_metrics", 112 | data: tokenMetrics, 113 | }, 114 | ]; 115 | } 116 | formatResults(results: PreprocessingResult[]): string { 117 | let formattedContent = ""; 118 | 119 | for (const result of results) { 120 | switch (result.type) { 121 | case "twitter_profiles": 122 | formattedContent += "\nTwitter Profile Context:\n"; 123 | formattedContent += result.data.timelines 124 | .map((tweet: any) => `@${tweet.authorUsername}: ${tweet.text}`) 125 | .join("\n"); 126 | break; 127 | case "token_metrics": 128 | formattedContent += "\nToken Metrics:\n"; 129 | formattedContent += JSON.stringify(result.data); 130 | break; 131 | } 132 | } 133 | 134 | return formattedContent; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/core/managers/quantum-personality.ts: -------------------------------------------------------------------------------- 1 | import type { Character } from "@/db/schema/schema"; 2 | import { type QuantumState } from "@/db/schema/schema"; 3 | import type { QuantumStateManager } from "./quantum"; 4 | 5 | export interface QuantumPersonalitySettings { 6 | temperature: number; 7 | personalityTraits: string[]; 8 | styleModifiers: { 9 | tone: string[]; 10 | guidelines: string[]; 11 | }; 12 | } 13 | 14 | export class QuantumPersonalityMapper { 15 | private character: Character; 16 | private quantumStateManager: QuantumStateManager; 17 | 18 | constructor(quantumStateManager: QuantumStateManager, character: Character) { 19 | this.quantumStateManager = quantumStateManager; 20 | this.character = character; 21 | } 22 | 23 | async mapQuantumToPersonality(): Promise { 24 | const quantumState = await this.quantumStateManager.getLatestState(); 25 | if (!quantumState) throw new Error("No quantum state found"); 26 | 27 | // Add debug logging 28 | 29 | // Map mood to temperature with wider range (0.6 to 0.8) 30 | const temperature = this.calculateTemperature(quantumState.moodValue); 31 | 32 | // Map creativity to personality traits and style 33 | const creativityLevel = this.determineCreativityLevel( 34 | quantumState.creativityValue 35 | ); 36 | 37 | // Get personality configuration based on creativity level 38 | const personalityConfig = this.getPersonalityConfig( 39 | creativityLevel, 40 | quantumState 41 | ); 42 | 43 | // Add debug logging for final configuration 44 | 45 | return { 46 | temperature, 47 | ...personalityConfig, 48 | }; 49 | } 50 | 51 | private calculateTemperature(moodValue: number): number { 52 | // Default to a wider range if not specified in character config 53 | const range = this.character.quantumPersonality?.temperatureRange ?? { 54 | min: 0.5, // Lower minimum (was 0.6) 55 | max: 0.9, // Higher maximum (was 0.8) 56 | }; 57 | 58 | // Add randomization factor for more variation 59 | const baseTemp = range.min + (moodValue / 255) * (range.max - range.min); 60 | 61 | // Add small random variation (±0.05) to prevent repetitive values 62 | const variation = Math.random() * 0.1 - 0.05; 63 | const finalTemp = Math.max( 64 | range.min, 65 | Math.min(range.max, baseTemp + variation) 66 | ); 67 | 68 | // Log the temperature calculation process 69 | 70 | return Number(finalTemp.toFixed(3)); // Round to 3 decimal places 71 | } 72 | 73 | private determineCreativityLevel( 74 | creativityValue: number 75 | ): "low" | "medium" | "high" { 76 | const thresholds = this.character.quantumPersonality 77 | ?.creativityThresholds ?? { 78 | low: 100, 79 | medium: 180, 80 | }; 81 | 82 | if (creativityValue < thresholds.low) return "low"; 83 | if (creativityValue < thresholds.medium) return "medium"; 84 | return "high"; 85 | } 86 | 87 | private getPersonalityConfig( 88 | creativityLevel: "low" | "medium" | "high", 89 | quantumState: QuantumState 90 | ): Omit { 91 | // Use character-specific settings if available, otherwise fall back to defaults 92 | const characterSettings = 93 | this.character.quantumPersonality?.creativityLevels[creativityLevel]; 94 | if (characterSettings) { 95 | return characterSettings; 96 | } 97 | 98 | // Fall back to default settings if character-specific ones aren't available 99 | switch (creativityLevel) { 100 | case "low": 101 | return { 102 | personalityTraits: ["witty", "sarcastic", "curt"], 103 | styleModifiers: { 104 | tone: ["precise", "analytical", "direct"], 105 | guidelines: [ 106 | "Keep responses concise and pointed", 107 | "Focus on technical accuracy", 108 | "Maintain sarcastic undertone", 109 | ], 110 | }, 111 | }; 112 | case "medium": 113 | return { 114 | personalityTraits: ["edgy", "confident", "witty"], 115 | styleModifiers: { 116 | tone: ["balanced", "conversational", "witty"], 117 | guidelines: [ 118 | "Mix technical and casual language", 119 | "Use moderate market references", 120 | "Balance humor and information", 121 | ], 122 | }, 123 | }; 124 | case "high": 125 | return { 126 | personalityTraits: ["edgy", "arrogant", "original"], 127 | styleModifiers: { 128 | tone: ["creative", "provocative", "unconventional"], 129 | guidelines: [ 130 | "Push creative boundaries", 131 | "Challenge conventional wisdom", 132 | "Emphasize unique perspectives", 133 | "Break fourth wall occasionally", 134 | ], 135 | }, 136 | }; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/core/managers/quantum.ts: -------------------------------------------------------------------------------- 1 | import { IBMQuantumClient, type IBMConfig } from "@/core/utils/IBMRest"; 2 | import type { dbSchemas } from "@/db"; 3 | import { 4 | quantumStates, 5 | type NewQuantumState, 6 | type QuantumState, 7 | } from "@/db/schema/schema"; 8 | import { and, desc, eq, gte, lte, sql } from "drizzle-orm"; 9 | import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; 10 | 11 | export class QuantumStateManager { 12 | private quantumClient: IBMQuantumClient; 13 | 14 | constructor( 15 | private db: PostgresJsDatabase, 16 | config?: IBMConfig 17 | ) { 18 | if (!config) { 19 | throw new Error("IBM Quantum config is required"); 20 | } 21 | this.quantumClient = new IBMQuantumClient(config); 22 | } 23 | 24 | async generateQuantumState(): Promise { 25 | try { 26 | // Generate the circuit QASM 27 | const circuitQASM = this.quantumClient.generateCircuitQASM(16); 28 | 29 | // Execute the circuit and get results 30 | const result = await this.quantumClient.submitJob(circuitQASM); 31 | 32 | // Get all measurement results and their counts 33 | const counts = result.meas.get_counts(); 34 | const measurements = Object.entries(counts); 35 | 36 | // Calculate statistics from the quantum measurements 37 | const total = measurements.reduce((sum, [_, count]) => sum + count, 0); 38 | let entropy = 0; 39 | let weightedSum = 0; 40 | 41 | measurements.forEach(([bitstring, count]) => { 42 | const prob = count / total; 43 | if (prob > 0) { 44 | entropy -= prob * Math.log2(prob); // Calculate Shannon entropy 45 | weightedSum += parseInt(bitstring, 2) * prob; 46 | } 47 | }); 48 | 49 | // Normalize entropy to 0-255 range for mood 50 | const normalizedEntropy = Math.floor((entropy / 16) * 255); // 16 is max entropy for 16 qubits 51 | 52 | // Use weighted sum for creativity (normalized to 0-255) 53 | const normalizedWeightedSum = Math.floor((weightedSum / 65535) * 255); 54 | 55 | // Get the most frequent measurement for random value 56 | const [mostFrequentBitstring] = measurements.reduce((max, curr) => 57 | curr[1] > max[1] ? curr : max 58 | ); 59 | 60 | return { 61 | timestamp: new Date(), 62 | randomValue: parseInt(mostFrequentBitstring, 2), 63 | moodValue: normalizedEntropy, 64 | creativityValue: normalizedWeightedSum, 65 | entropyHash: mostFrequentBitstring, 66 | isFallback: false, 67 | }; 68 | } catch (error) { 69 | console.error("Quantum generation failed, using fallback:", error); 70 | // if we get an error use the last good state 71 | const lastState = await this.getLatestState(); 72 | if (lastState) { 73 | return lastState; 74 | } else { 75 | return { 76 | timestamp: new Date(), 77 | randomValue: 65367, 78 | moodValue: 145, 79 | creativityValue: 240, 80 | entropyHash: "1111111101010111", 81 | isFallback: true, 82 | }; 83 | } 84 | } 85 | } 86 | 87 | // hard defaults for fallback state 88 | private generateFallbackState(): NewQuantumState { 89 | const randomBytes = new Uint16Array(1); // Use Uint16Array to directly get a 16-bit value 90 | crypto.getRandomValues(randomBytes); 91 | 92 | // Convert the single 16-bit number to binary 93 | //const entropyBinary = randomBytes[0].toString(2).padStart(16, "0"); 94 | 95 | return { 96 | timestamp: new Date(), 97 | randomValue: 65367, 98 | moodValue: 145, 99 | creativityValue: 240, 100 | entropyHash: "1111111101010111", 101 | isFallback: true, 102 | }; 103 | } 104 | 105 | async storeState(state: NewQuantumState) { 106 | return await this.db.insert(quantumStates).values(state).returning(); 107 | } 108 | 109 | async getLatestState(): Promise { 110 | const results = await this.db 111 | .select() 112 | .from(quantumStates) 113 | .orderBy(desc(quantumStates.timestamp)) 114 | .where(eq(quantumStates.isFallback, false)) 115 | .limit(1); 116 | 117 | return results[0]; 118 | } 119 | 120 | async getStatesInRange(startTime: Date, endTime: Date) { 121 | return await this.db 122 | .select() 123 | .from(quantumStates) 124 | .where( 125 | and( 126 | gte(quantumStates.timestamp, startTime), 127 | lte(quantumStates.timestamp, endTime) 128 | ) 129 | ) 130 | .orderBy(quantumStates.timestamp); 131 | } 132 | 133 | async cleanupOldStates(daysToKeep: number = 7) { 134 | const cutoffDate = new Date(); 135 | cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); 136 | 137 | return await this.db 138 | .delete(quantumStates) 139 | .where(lte(quantumStates.timestamp, cutoffDate)) 140 | .returning(); 141 | } 142 | 143 | async getStateStats(startTime: Date, endTime: Date) { 144 | const stats = await this.db 145 | .select({ 146 | count: sql`count(*)::int`, 147 | fallbackCount: sql`sum(case when ${quantumStates.isFallback} then 1 else 0 end)::int`, 148 | avgRandomValue: sql`avg(${quantumStates.randomValue})::float`, 149 | avgCreativityValue: sql`avg(${quantumStates.creativityValue})::float`, 150 | }) 151 | .from(quantumStates) 152 | .where( 153 | and( 154 | gte(quantumStates.timestamp, startTime), 155 | lte(quantumStates.timestamp, endTime) 156 | ) 157 | ); 158 | 159 | return stats[0]; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/core/managers/style.ts: -------------------------------------------------------------------------------- 1 | import { CharacterManager } from "@/core/managers/character"; 2 | import type { Character } from "@/db/schema/schema"; 3 | import type { 4 | CustomResponseType, 5 | Platform, 6 | PlatformStyles, 7 | PlatformStylesInput, 8 | ResponseStyles, 9 | ResponseType, 10 | StyleSettings, 11 | } from "@/types"; 12 | import { QuantumPersonalityMapper } from "./quantum-personality"; 13 | 14 | export class StyleManager { 15 | constructor( 16 | private characterManager: CharacterManager, 17 | private quantumPersonalityMapper?: QuantumPersonalityMapper 18 | ) {} 19 | 20 | async updatePlatformStyles( 21 | characterId: string, 22 | platform: Platform, 23 | styles: PlatformStylesInput 24 | ) { 25 | const character = await this.characterManager.getCharacter(characterId); 26 | if (!character) throw new Error("Character not found"); 27 | 28 | const responseStyles = character.responseStyles || { 29 | default: { tone: [], personality: [], guidelines: [] }, 30 | platforms: {}, 31 | }; 32 | 33 | responseStyles.platforms[platform] = styles as PlatformStyles; 34 | 35 | return this.characterManager.updateCharacter(characterId, { 36 | responseStyles, 37 | updatedAt: new Date(), 38 | }); 39 | } 40 | 41 | async getPlatformFromResponseType( 42 | characterId: string, 43 | responseType: ResponseType 44 | ): Promise { 45 | const character = await this.characterManager.getCharacter(characterId); 46 | if (!character) throw new Error("Character not found"); 47 | 48 | // Handle custom types 49 | if (responseType.startsWith("custom_")) { 50 | // Type guard to ensure responseType is a custom type key 51 | const customTypeKey = 52 | responseType as keyof typeof character.responseStyles.customTypes; 53 | const customType = character.responseStyles?.customTypes?.[customTypeKey]; 54 | if (customType) { 55 | return customType.platform; 56 | } 57 | } 58 | 59 | // Handle standard types 60 | if (responseType.startsWith("tweet_")) return "twitter"; 61 | if (responseType.startsWith("discord_")) return "discord"; 62 | if (responseType.startsWith("telegram_")) return "telegram"; 63 | if (responseType.startsWith("slack_")) return "slack"; 64 | return "telegram"; // default fallback 65 | } 66 | 67 | async getStyleSettings( 68 | responseStyles: ResponseStyles, 69 | platform: Platform, 70 | responseType: ResponseType 71 | ): Promise { 72 | const defaultStyles: StyleSettings = { 73 | enabled: true, 74 | tone: responseStyles.default.tone, 75 | guidelines: responseStyles.default.guidelines, 76 | formatting: {}, 77 | }; 78 | 79 | // Get quantum personality modifiers 80 | const personalitySettings = 81 | await this.quantumPersonalityMapper?.mapQuantumToPersonality(); 82 | 83 | // Merge quantum style modifiers 84 | defaultStyles.tone = [ 85 | ...(defaultStyles.tone ?? []), 86 | ...(personalitySettings?.styleModifiers.tone ?? []), 87 | ]; 88 | defaultStyles.guidelines = [ 89 | ...defaultStyles.guidelines, 90 | ...(personalitySettings?.styleModifiers.guidelines || []), 91 | ]; 92 | 93 | const platformStyles = responseStyles.platforms[platform]; 94 | if (!platformStyles) return defaultStyles; 95 | 96 | // Handle custom types 97 | if (responseType.startsWith("custom_")) { 98 | const customType = 99 | responseStyles.customTypes?.[responseType as CustomResponseType]; 100 | if (customType && customType.platform !== platform) { 101 | throw new Error( 102 | `Custom type ${responseType} is registered for platform ${customType.platform}, not ${platform}` 103 | ); 104 | } 105 | } 106 | 107 | // Merge platform defaults 108 | const withPlatformDefaults: StyleSettings = { 109 | ...defaultStyles, 110 | tone: [ 111 | ...(defaultStyles.tone ?? []), 112 | ...(platformStyles.defaultTone ?? []), 113 | ], 114 | guidelines: [ 115 | ...defaultStyles.guidelines, 116 | ...platformStyles.defaultGuidelines, 117 | ], 118 | }; 119 | 120 | const typeStyles = platformStyles.styles[responseType]; 121 | if (!typeStyles) return withPlatformDefaults; 122 | 123 | return { 124 | enabled: typeStyles.enabled ?? true, 125 | tone: [...(withPlatformDefaults.tone ?? []), ...(typeStyles.tone ?? [])], 126 | guidelines: [ 127 | ...withPlatformDefaults.guidelines, 128 | ...typeStyles.guidelines, 129 | ], 130 | formatting: { 131 | ...withPlatformDefaults.formatting, 132 | ...typeStyles.formatting, 133 | }, 134 | rules: [...(typeStyles.formatting?.customRules || [])], 135 | }; 136 | } 137 | 138 | async buildStyleContext( 139 | character: Character, 140 | styleSettings: StyleSettings, 141 | userContext: Record 142 | ) { 143 | return { 144 | name: character.name, 145 | personality: character.personalityTraits.join("\n"), 146 | tone: (styleSettings.tone ?? []).join("\n"), 147 | guidelines: styleSettings.guidelines.join("\n"), 148 | formatting: styleSettings.formatting, 149 | ...userContext, 150 | }; 151 | } 152 | 153 | async registerCustomResponseType( 154 | characterId: string, 155 | customType: CustomResponseType, 156 | config: { 157 | platform: Platform; 158 | description?: string; 159 | settings: StyleSettings; 160 | } 161 | ) { 162 | const character = await this.characterManager.getCharacter(characterId); 163 | if (!character) throw new Error("Character not found"); 164 | 165 | const responseStyles = character.responseStyles || { 166 | default: { tone: [], personality: [], guidelines: [] }, 167 | platforms: {}, 168 | customTypes: {}, 169 | }; 170 | 171 | // Register the custom type 172 | responseStyles.customTypes = responseStyles.customTypes || {}; 173 | responseStyles.customTypes[customType] = { 174 | platform: config.platform, 175 | description: config.description, 176 | }; 177 | 178 | // Add the style settings to the appropriate platform 179 | if (!responseStyles.platforms[config.platform]) { 180 | responseStyles.platforms[config.platform] = { 181 | enabled: true, 182 | defaultTone: [], 183 | defaultGuidelines: [], 184 | styles: {}, 185 | }; 186 | } 187 | 188 | responseStyles.platforms[config.platform]!.styles[customType] = 189 | config.settings; 190 | 191 | return this.characterManager.updateCharacter(characterId, { 192 | responseStyles, 193 | }); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/core/managers/tools.ts: -------------------------------------------------------------------------------- 1 | import type { Tool, ToolResult } from "@/types/tools"; 2 | import path from "path"; 3 | 4 | export class ToolManager { 5 | private tools: Map = new Map(); 6 | private toolsDir: string; 7 | 8 | constructor({ toolsDir }: { toolsDir?: string }) { 9 | this.toolsDir = toolsDir || path.join(process.cwd(), "ai/tools"); 10 | } 11 | 12 | async loadTool(toolName: string): Promise { 13 | if (this.tools.has(toolName)) { 14 | return; 15 | } 16 | 17 | const toolPath = path.join(this.toolsDir, toolName, "index.ts"); 18 | 19 | try { 20 | const toolModule = await import(toolPath); 21 | const tool: Tool = toolModule.default; 22 | 23 | if (!tool || !tool.name || !tool.execute) { 24 | throw new Error(`Invalid tool module structure for ${toolName}`); 25 | } 26 | 27 | this.tools.set(toolName, tool); 28 | } catch (error) { 29 | console.error(`Error loading tool ${toolName}:`, { 30 | error: 31 | error instanceof Error 32 | ? { 33 | message: error.message, 34 | name: error.name, 35 | stack: error.stack, 36 | } 37 | : error, 38 | toolPath, 39 | }); 40 | throw new Error(`Failed to load tool: ${toolName}`); 41 | } 42 | } 43 | 44 | async executeTool(toolName: string, params?: any): Promise { 45 | const tool = this.tools.get(toolName); 46 | if (!tool) { 47 | console.error(`Tool not found: ${toolName}`); 48 | throw new Error(`Tool not found: ${toolName}`); 49 | } 50 | 51 | try { 52 | const result = await tool.execute(params); 53 | return result; 54 | } catch (error) { 55 | console.error(`Error executing tool ${toolName}:`, { 56 | error: 57 | error instanceof Error 58 | ? { 59 | message: error.message, 60 | name: error.name, 61 | stack: error.stack, 62 | } 63 | : error, 64 | }); 65 | return { 66 | success: false, 67 | data: null, 68 | error: 69 | error instanceof Error 70 | ? error.message 71 | : "Unknown error executing tool", 72 | }; 73 | } 74 | } 75 | 76 | async executeTools( 77 | tools: string[], 78 | context?: Record 79 | ): Promise> { 80 | const results: Record = {}; 81 | 82 | for (const toolName of tools) { 83 | try { 84 | if (!this.tools.has(toolName)) { 85 | await this.loadTool(toolName); 86 | } 87 | results[toolName] = await this.executeTool(toolName, context); 88 | } catch (error) { 89 | console.error(`Failed to execute tool ${toolName}:`, error); 90 | results[toolName] = { 91 | success: false, 92 | data: null, 93 | error: error instanceof Error ? error.message : "Unknown error", 94 | }; 95 | } 96 | } 97 | 98 | return results; 99 | } 100 | 101 | formatToolResults(results: Record): string { 102 | let formatted = "\nTool Data:\n"; 103 | 104 | for (const [toolName, result] of Object.entries(results)) { 105 | formatted += `\n${toolName}:\n`; 106 | if (result.success) { 107 | formatted += JSON.stringify(result.data, null, 2); 108 | } else { 109 | formatted += `Error: ${result.error}`; 110 | } 111 | formatted += "\n"; 112 | } 113 | 114 | return formatted; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/core/platform/api/server.ts: -------------------------------------------------------------------------------- 1 | import type { Platform } from "@/types"; 2 | import type { Server } from "bun"; 3 | import crypto from "crypto"; 4 | import type { ai } from "../../ai"; 5 | import { ImageManager } from "../../managers/image"; 6 | export interface ServerConfig { 7 | enabled: boolean; 8 | port: number; 9 | hostname?: string; 10 | cors?: { 11 | allowedOrigins: string[]; 12 | }; 13 | apiKey?: string; 14 | } 15 | 16 | export class APIServer { 17 | private server: Server | null = null; 18 | private config: ServerConfig; 19 | private ai: ai; 20 | private imageManager: ImageManager; 21 | 22 | constructor(ai: ai, config: ServerConfig) { 23 | this.ai = ai; 24 | this.imageManager = new ImageManager(ai.llmManager, ai.eventService); 25 | this.config = config; 26 | } 27 | 28 | async start() { 29 | if (this.server) { 30 | console.log("Server is already running"); 31 | return; 32 | } 33 | 34 | this.server = Bun.serve({ 35 | port: this.config.port, 36 | hostname: this.config.hostname || "localhost", 37 | fetch: (request: Request) => this.handleRequest(request), 38 | }); 39 | 40 | console.log(`API Server started on port ${this.config.port}`); 41 | } 42 | 43 | async stop() { 44 | if (this.server) { 45 | this.server.stop(); 46 | this.server = null; 47 | console.log("API Server stopped"); 48 | } 49 | } 50 | 51 | private async handleRequest(request: Request): Promise { 52 | try { 53 | if (request.method === "OPTIONS") { 54 | return this.handleCORS(request); 55 | } 56 | 57 | if (this.config.apiKey) { 58 | const authHeader = request.headers.get("Authorization"); 59 | if (!this.validateApiKey(authHeader)) { 60 | return new Response("Unauthorized", { status: 401 }); 61 | } 62 | } 63 | 64 | const url = new URL(request.url); 65 | 66 | if (url.pathname === "/api/image" && request.method === "POST") { 67 | return this.handleImageGeneration(request); 68 | } 69 | if (url.pathname === "/api/img" && request.method === "GET") { 70 | return this.handleImageGeneration(); 71 | } 72 | 73 | return new Response("Not Found", { status: 404 }); 74 | } catch (error) { 75 | console.error("API Error:", error); 76 | return new Response("Internal Server Error", { status: 500 }); 77 | } 78 | } 79 | 80 | private async handleImageGeneration(request?: Request): Promise { 81 | try { 82 | let text = ""; 83 | if (!request) { 84 | text = ""; 85 | } else { 86 | const body = await request.json(); 87 | text = body.text; 88 | } 89 | 90 | const result = await this.imageManager.generateImage(text, { 91 | messageId: crypto.randomUUID(), 92 | sessionId: crypto.randomUUID(), 93 | platform: "api" as Platform, 94 | user: { 95 | id: "anonymous", 96 | username: "anonymous", 97 | }, 98 | }); 99 | 100 | return new Response(JSON.stringify(result), { 101 | status: 200, 102 | headers: { "Content-Type": "application/json" }, 103 | }); 104 | } catch (error) { 105 | console.error("Image generation error:", error); 106 | 107 | // Return appropriate error response 108 | const errorMessage = 109 | error instanceof Error ? error.message : "Unknown error"; 110 | const status = errorMessage.includes("rate limit") 111 | ? 429 112 | : errorMessage.includes("moderation") 113 | ? 400 114 | : 500; 115 | 116 | return new Response(JSON.stringify({ error: errorMessage }), { 117 | status, 118 | headers: { "Content-Type": "application/json" }, 119 | }); 120 | } 121 | } 122 | 123 | private handleCORS(request: Request): Response { 124 | const origin = request.headers.get("Origin"); 125 | const allowedOrigins = this.config.cors?.allowedOrigins || ["*"]; 126 | 127 | const headers = new Headers({ 128 | "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 129 | "Access-Control-Allow-Headers": "Content-Type, Authorization", 130 | }); 131 | 132 | if ( 133 | origin && 134 | (allowedOrigins.includes("*") || allowedOrigins.includes(origin)) 135 | ) { 136 | headers.set("Access-Control-Allow-Origin", origin); 137 | } 138 | 139 | return new Response(null, { headers }); 140 | } 141 | 142 | private validateApiKey(authHeader: string | null): boolean { 143 | if (!this.config.apiKey) return true; 144 | if (!authHeader) return false; 145 | 146 | const [type, key] = authHeader.split(" "); 147 | return type === "Bearer" && key === this.config.apiKey; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/core/platform/telegram/PromptService.ts: -------------------------------------------------------------------------------- 1 | interface ChatHistory { 2 | role: "user" | "assistant"; 3 | content: string; 4 | timestamp: number; 5 | messageId: number; 6 | username?: string; 7 | } 8 | 9 | export class PromptService { 10 | private readonly MAX_HISTORY_LENGTH = 10; 11 | private chatHistory: Map = new Map(); 12 | 13 | constructor() {} 14 | 15 | public addToHistory(chatId: number, message: ChatHistory) { 16 | if (!this.chatHistory.has(chatId)) { 17 | this.chatHistory.set(chatId, []); 18 | } 19 | 20 | const history = this.chatHistory.get(chatId)!; 21 | history.push(message); 22 | 23 | if (history.length > this.MAX_HISTORY_LENGTH) { 24 | history.shift(); 25 | } 26 | 27 | this.chatHistory.set(chatId, history); 28 | } 29 | 30 | public formatChatHistory(chatId: number): string { 31 | const history = this.chatHistory.get(chatId) || []; 32 | if (history.length === 0) return ""; 33 | 34 | return history 35 | .map((msg) => { 36 | const timestamp = new Date(msg.timestamp * 1000).toLocaleTimeString(); 37 | return `[${timestamp}] ${msg.username} (${msg.role}): ${msg.content}`; 38 | }) 39 | .join("\n\n"); 40 | } 41 | 42 | public buildSystemPrompt( 43 | username: string, 44 | firstName?: string, 45 | lastName?: string, 46 | isReply?: boolean, 47 | chatHistory?: string 48 | ): string { 49 | const userContext = username 50 | ? `User @${username}` 51 | : `User ${firstName}${lastName ? ` ${lastName}` : ""}`; 52 | 53 | const replyContext = isReply 54 | ? ` The user is replying to a previous message.` 55 | : ""; 56 | 57 | const historyContext = chatHistory 58 | ? `\n\nChat History:\n${chatHistory}` 59 | : "\n\nThis is the start of the conversation."; 60 | 61 | return `Private chat with ${userContext}.${replyContext}${historyContext}`; 62 | } 63 | 64 | public cleanupOldHistories() { 65 | const now = Date.now(); 66 | for (const [chatId, history] of this.chatHistory) { 67 | const oneDayAgo = now / 1000 - 86400; 68 | const recentHistory = history.filter((msg) => msg.timestamp > oneDayAgo); 69 | if (recentHistory.length !== history.length) { 70 | this.chatHistory.set(chatId, recentHistory); 71 | } 72 | } 73 | } 74 | 75 | public getHistoryContext(chatId: number): Array { 76 | return this.chatHistory.get(chatId) || []; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/core/platform/telegram/handlers/imageHandler.ts: -------------------------------------------------------------------------------- 1 | import type { ai } from "@/core/ai"; 2 | import type { ImageManager } from "@/core/managers/image"; 3 | import type { Context } from "telegraf"; 4 | 5 | export class ImageHandler { 6 | constructor( 7 | private imageManager: ImageManager, 8 | private ai: ai // Your existing AI instance 9 | ) {} 10 | 11 | async handleImageGeneration(ctx: Context) { 12 | try { 13 | if (!ctx.message || !("text" in ctx.message)) { 14 | await ctx.reply("Please send a text message."); 15 | return; 16 | } 17 | 18 | const text = ctx.message.text.replace(/^\/img\s*/, "").trim(); 19 | 20 | if (!text) { 21 | await ctx.reply("Please provide a description after the /img command."); 22 | return; 23 | } 24 | 25 | const result = await this.imageManager.generateImage(text, { 26 | messageId: ctx.message.message_id.toString(), 27 | platform: "telegram", 28 | sessionId: `${ctx.chat?.id}-${Date.now()}`, 29 | user: { 30 | id: ctx.from?.id.toString() || "", 31 | username: ctx.from?.username, 32 | }, 33 | }); 34 | 35 | if (result.success && result.url) { 36 | await ctx.replyWithPhoto(result.url); 37 | } else { 38 | throw new Error(result.error || "Unknown error"); 39 | } 40 | } catch (error) { 41 | console.error("Error in handleImageGeneration:", error); 42 | await ctx.reply( 43 | "Sorry, there was an error generating your image. Please try again later." 44 | ); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/core/platform/telegram/handlers/messageHandler.ts: -------------------------------------------------------------------------------- 1 | import type { ai } from "@/core/ai"; 2 | import type { InteractionDefaults } from "@/types"; 3 | import { Context } from "telegraf"; 4 | import type { Message } from "telegraf/types"; 5 | import { PromptService } from "../PromptService"; 6 | 7 | interface ProcessingMessage { 8 | timestamp: number; 9 | messageId: number; 10 | } 11 | 12 | export class MessageHandler { 13 | private processingMessages: Map = new Map(); 14 | private readonly PROCESSING_TIMEOUT = 30000; 15 | private readonly MAX_MESSAGE_LENGTH = 4096; 16 | private readonly RATE_LIMIT_WINDOW = 60000; 17 | private readonly MAX_MESSAGES_PER_WINDOW = 10; 18 | private messageCounter: Map = 19 | new Map(); 20 | private promptService: PromptService; 21 | 22 | constructor(private ai: ai, private defaults?: InteractionDefaults) { 23 | this.setupCleanupInterval(); 24 | this.promptService = new PromptService(); 25 | } 26 | 27 | public async handle(ctx: Context, characterId: string) { 28 | const message = ctx.message as Message.TextMessage; 29 | if (!this.isValidMessage(message)) { 30 | return; 31 | } 32 | 33 | const userId = message.from?.id; 34 | const chatId = message.chat.id; 35 | const messageId = message.message_id; 36 | 37 | try { 38 | if (userId && !this.checkRateLimit(userId)) { 39 | await ctx.reply( 40 | "You are sending too many messages. Please wait a moment before trying again." 41 | ); 42 | return; 43 | } 44 | 45 | if (userId && this.isProcessing(userId)) { 46 | await ctx.reply( 47 | "Still processing your previous message! Please wait a moment..." 48 | ); 49 | return; 50 | } 51 | 52 | if (userId) { 53 | this.setProcessingState(userId, messageId); 54 | await ctx.sendChatAction("typing"); 55 | } 56 | 57 | // Add user message to history 58 | this.promptService.addToHistory(chatId, { 59 | role: "user", 60 | content: message.text, 61 | timestamp: message.date, 62 | messageId: message.message_id, 63 | username: 64 | message.from?.username || `${message.from?.first_name || "User"}`, 65 | }); 66 | 67 | const response = await this.processMessage(message, characterId); 68 | 69 | if (!response) { 70 | console.error("No response from AI"); 71 | return; 72 | } 73 | 74 | // Add assistant's response to history 75 | this.promptService.addToHistory(chatId, { 76 | role: "assistant", 77 | content: response, 78 | timestamp: Math.floor(Date.now() / 1000), 79 | messageId: messageId + 1, 80 | username: "Ducky", 81 | }); 82 | 83 | return response; 84 | } catch (error) { 85 | await this.handleError(ctx, error); 86 | } finally { 87 | if (userId) { 88 | this.clearProcessingState(userId); 89 | } 90 | } 91 | } 92 | 93 | private async processMessage( 94 | message: Message.TextMessage, 95 | characterId: string 96 | ) { 97 | // Get default options either from config or use basic defaults 98 | const defaultOptions = this.defaults || { 99 | mode: "enhanced" as const, 100 | platform: "telegram" as const, 101 | responseType: "telegram_chat", 102 | characterId, 103 | }; 104 | 105 | const chatHistory = this.promptService.formatChatHistory(message.chat.id); 106 | 107 | // Complete the options with required fields 108 | const interactionOptions = { 109 | ...defaultOptions, 110 | characterId, 111 | userId: message.from?.id.toString() || "", 112 | chatId: message.chat.id.toString(), 113 | messageId: message.message_id.toString(), 114 | username: message.from?.username, 115 | }; 116 | 117 | // Process with the AI 118 | const response = await this.ai.interact( 119 | { 120 | system: this.promptService.buildSystemPrompt( 121 | message.from?.username || "", 122 | message.from?.first_name || "", 123 | message.from?.last_name || "", 124 | !!message.reply_to_message, 125 | chatHistory 126 | ), 127 | user: message.text, 128 | }, 129 | interactionOptions 130 | ); 131 | 132 | return response?.content ?? null; 133 | } 134 | 135 | private isValidMessage(message: any): message is Message.TextMessage { 136 | if (!message?.text || typeof message.text !== "string") { 137 | return false; 138 | } 139 | 140 | if (message.text.trim().length === 0) { 141 | return false; 142 | } 143 | 144 | if (!message.from?.id || !message.chat?.id || !message.message_id) { 145 | return false; 146 | } 147 | 148 | return true; 149 | } 150 | 151 | private isProcessing(userId: number): boolean { 152 | const processingMessage = this.processingMessages.get(userId); 153 | if (!processingMessage) return false; 154 | 155 | const elapsed = Date.now() - processingMessage.timestamp; 156 | return elapsed < this.PROCESSING_TIMEOUT; 157 | } 158 | 159 | private setProcessingState(userId: number, messageId: number) { 160 | this.processingMessages.set(userId, { 161 | timestamp: Date.now(), 162 | messageId, 163 | }); 164 | } 165 | 166 | private clearProcessingState(userId: number) { 167 | this.processingMessages.delete(userId); 168 | } 169 | 170 | private setupCleanupInterval() { 171 | setInterval(() => { 172 | const now = Date.now(); 173 | 174 | // Cleanup processing messages 175 | for (const [userId, message] of this.processingMessages) { 176 | if (now - message.timestamp > this.PROCESSING_TIMEOUT) { 177 | this.processingMessages.delete(userId); 178 | } 179 | } 180 | 181 | // Cleanup rate limit counters 182 | for (const [userId, data] of this.messageCounter) { 183 | if (now - data.windowStart > this.RATE_LIMIT_WINDOW) { 184 | this.messageCounter.delete(userId); 185 | } 186 | } 187 | }, 60000); 188 | } 189 | 190 | private checkRateLimit(userId: number): boolean { 191 | const now = Date.now(); 192 | const userMessages = this.messageCounter.get(userId); 193 | 194 | if ( 195 | !userMessages || 196 | now - userMessages.windowStart > this.RATE_LIMIT_WINDOW 197 | ) { 198 | this.messageCounter.set(userId, { 199 | count: 1, 200 | windowStart: now, 201 | }); 202 | return true; 203 | } 204 | 205 | if (userMessages.count >= this.MAX_MESSAGES_PER_WINDOW) { 206 | return false; 207 | } 208 | 209 | userMessages.count++; 210 | return true; 211 | } 212 | 213 | private async handleError(ctx: Context, error: any) { 214 | console.error("Error handling message:", error); 215 | 216 | let errorMessage = "An error occurred processing your message."; 217 | 218 | if (error.code === "ETIMEDOUT") { 219 | errorMessage = "Request timed out. Please try again."; 220 | } else if (error.response?.status === 429) { 221 | errorMessage = 222 | "Too many requests. Please wait a moment before trying again."; 223 | } else if (error.message?.includes("context length")) { 224 | errorMessage = 225 | "Message is too long to process. Please try a shorter message."; 226 | } 227 | 228 | try { 229 | await ctx.reply(errorMessage); 230 | } catch (replyError) { 231 | console.error("Error sending error message:", replyError); 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/core/platform/telegram/telegram.ts: -------------------------------------------------------------------------------- 1 | import type { ai } from "@/core/ai"; 2 | import { ImageManager } from "@/core/managers/image"; 3 | import type { InteractionDefaults } from "@/types"; 4 | import { Context, Telegraf } from "telegraf"; 5 | import { message } from "telegraf/filters"; 6 | import { ImageHandler } from "./handlers/imageHandler"; 7 | import { MessageHandler } from "./handlers/messageHandler"; 8 | 9 | export class TelegramClient { 10 | private bot: Telegraf; 11 | private messageHandler: MessageHandler; 12 | private readonly MAX_MESSAGE_LENGTH = 4096; 13 | private readonly PROCESSING_TIMEOUT = 30000; 14 | private processingMessages: Map = new Map(); 15 | private isRunning: boolean = false; 16 | private imageHandler: ImageHandler; 17 | 18 | constructor( 19 | token: string, 20 | private ai: ai, 21 | private defaults?: InteractionDefaults 22 | ) { 23 | this.bot = new Telegraf(token); 24 | this.messageHandler = new MessageHandler(ai, defaults); 25 | const imageManager = new ImageManager(ai.llmManager, ai.eventService); 26 | this.imageHandler = new ImageHandler(imageManager, ai); 27 | this.setupMiddleware(); 28 | this.setupHandlers(); 29 | } 30 | 31 | public async start() { 32 | try { 33 | this.bot 34 | .launch() 35 | .then(() => { 36 | this.isRunning = true; 37 | console.log("Telegram bot started successfully!"); 38 | }) 39 | .catch((error) => { 40 | console.error("Failed to start Telegram bot:", error); 41 | this.isRunning = false; 42 | }); 43 | 44 | process.once("SIGINT", () => this.stop()); 45 | process.once("SIGTERM", () => this.stop()); 46 | } catch (error) { 47 | console.error("Failed to initialize Telegram bot:", error); 48 | throw error; 49 | } 50 | } 51 | 52 | public async stop() { 53 | if (this.isRunning) { 54 | await this.bot.stop("SIGTERM"); 55 | this.isRunning = false; 56 | console.log("Telegram bot stopped successfully"); 57 | } 58 | } 59 | 60 | private setupMiddleware() { 61 | this.bot.use(async (ctx, next) => { 62 | try { 63 | const chatId = ctx.chat?.id.toString(); 64 | if (!chatId) return; 65 | 66 | if (!chatId.startsWith("-")) { 67 | return; 68 | } 69 | 70 | const adminChatId = this.ai.character.identity?.telegram_admin_chat; 71 | if (!adminChatId) { 72 | console.error("No admin chat configured"); 73 | return; 74 | } 75 | 76 | if (chatId === adminChatId) { 77 | return next(); 78 | } 79 | 80 | try { 81 | await ctx.leaveChat(); 82 | } catch (error) { 83 | console.error("Failed to leave chat:", error); 84 | } 85 | } catch (error) { 86 | console.error("Middleware error:", error); 87 | } 88 | }); 89 | } 90 | 91 | private setupHandlers() { 92 | this.registerCommands(); 93 | 94 | this.bot.on(message("text"), async (ctx) => { 95 | if (ctx.message.text.startsWith("/")) { 96 | console.log("Command detected:", ctx.message.text); 97 | return; 98 | } 99 | if (ctx.message.text.startsWith("/")) return; 100 | 101 | try { 102 | const userId = ctx.from?.id; 103 | if (!userId || this.isProcessing(userId)) { 104 | await ctx.reply( 105 | "Still processing your previous message! Please wait..." 106 | ); 107 | return; 108 | } 109 | 110 | this.setProcessing(userId); 111 | await ctx.sendChatAction("typing"); 112 | 113 | const response = await this.messageHandler.handle( 114 | ctx, 115 | this.ai.character.id 116 | ); 117 | if (response) { 118 | await this.sendResponse(ctx, response); 119 | } 120 | } catch (error) { 121 | await this.handleError(ctx, error); 122 | } finally { 123 | if (ctx.from?.id) { 124 | this.clearProcessing(ctx.from.id); 125 | } 126 | } 127 | }); 128 | 129 | this.bot.catch((error: any) => { 130 | console.error("Telegram bot error:", error); 131 | }); 132 | } 133 | 134 | private registerCommands() { 135 | console.log("registering commands"); 136 | this.bot.command("img", async (ctx) => { 137 | console.log("here"); 138 | const prompt = ctx.message.text.split(" ").slice(1).join(" ").trim(); 139 | 140 | if (!prompt) { 141 | await ctx.reply("Please provide an image description after /img"); 142 | return; 143 | } 144 | 145 | try { 146 | await ctx.reply("🎨 Generating your fat duck..."); 147 | await ctx.sendChatAction("upload_photo"); 148 | await this.imageHandler.handleImageGeneration(ctx); 149 | } catch (error) { 150 | await this.handleError(ctx, error); 151 | } 152 | }); 153 | 154 | this.bot.command("help", (ctx) => { 155 | const helpText = [ 156 | "*Available Commands:*", 157 | "• `/img ` - Generate an image", 158 | "", 159 | "*Examples:*", 160 | "• `/img a serene mountain landscape at sunset`", 161 | ].join("\n"); 162 | 163 | return ctx.reply(helpText, { parse_mode: "Markdown" }); 164 | }); 165 | 166 | this.bot.telegram.setMyCommands([ 167 | { command: "img", description: "Generate an image from description" }, 168 | { command: "help", description: "Show available commands" }, 169 | ]); 170 | } 171 | 172 | private async sendResponse(ctx: Context, text: string) { 173 | try { 174 | if (text.length <= this.MAX_MESSAGE_LENGTH) { 175 | await ctx.reply(text, { parse_mode: "Markdown" }); 176 | return; 177 | } 178 | 179 | const chunks = this.splitMessage(text); 180 | for (const chunk of chunks) { 181 | await ctx.reply(chunk, { parse_mode: "Markdown" }); 182 | await new Promise((resolve) => setTimeout(resolve, 100)); 183 | } 184 | } catch (error) { 185 | await ctx.reply(text.replace(/[*_`\[\]]/g, "")); 186 | } 187 | } 188 | 189 | private splitMessage(text: string): string[] { 190 | const chunks: string[] = []; 191 | let currentChunk = ""; 192 | 193 | const paragraphs = text.split("\n\n"); 194 | for (const paragraph of paragraphs) { 195 | if ( 196 | currentChunk.length + paragraph.length + 2 <= 197 | this.MAX_MESSAGE_LENGTH 198 | ) { 199 | currentChunk += (currentChunk ? "\n\n" : "") + paragraph; 200 | } else { 201 | if (currentChunk) chunks.push(currentChunk); 202 | currentChunk = paragraph; 203 | } 204 | } 205 | 206 | if (currentChunk) chunks.push(currentChunk); 207 | return chunks; 208 | } 209 | 210 | private isProcessing(userId: number): boolean { 211 | const timestamp = this.processingMessages.get(userId); 212 | if (!timestamp) return false; 213 | return Date.now() - timestamp < this.PROCESSING_TIMEOUT; 214 | } 215 | 216 | private setProcessing(userId: number) { 217 | this.processingMessages.set(userId, Date.now()); 218 | } 219 | 220 | private clearProcessing(userId: number) { 221 | this.processingMessages.delete(userId); 222 | } 223 | 224 | private async handleError(ctx: Context, error: any) { 225 | console.error("Error handling message:", error); 226 | 227 | let errorMessage = "An error occurred processing your message."; 228 | if (error.code === "ETIMEDOUT") { 229 | errorMessage = "Request timed out. Please try again."; 230 | } else if (error.message?.includes("context length")) { 231 | errorMessage = 232 | "Message is too long to process. Please try a shorter message."; 233 | } 234 | 235 | try { 236 | await ctx.reply(errorMessage); 237 | } catch (replyError) { 238 | console.error("Error sending error message:", replyError); 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/core/platform/twitter/api/src/auth/strategies.ts: -------------------------------------------------------------------------------- 1 | import { Cookie, CookieJar } from "tough-cookie"; 2 | import { TwitterApi } from "twitter-api-v2"; 3 | import type { APIv2Credentials, TwitterAuthStrategy } from "../interfaces"; 4 | 5 | export class CookieAuthStrategy implements TwitterAuthStrategy { 6 | private cookieJar: CookieJar; 7 | private readonly bearerToken = 8 | "AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF"; 9 | private readonly domains = ["twitter.com", ".twitter.com", "x.com", ".x.com"]; 10 | constructor() { 11 | this.cookieJar = new CookieJar(); 12 | } 13 | 14 | async setCookies(cookies: Array): Promise { 15 | for (const cookie of cookies) { 16 | // For each cookie, try setting it for all domains 17 | for (const domain of this.domains) { 18 | try { 19 | const cookieString = this.constructCookieString({ 20 | ...cookie, 21 | domain: domain, 22 | }); 23 | await this.cookieJar.setCookie(cookieString, `https://${domain}`); 24 | } catch (error) { 25 | console.warn(`Failed to set cookie for domain ${domain}:`, error); 26 | } 27 | } 28 | } 29 | 30 | // Verify critical cookies were set 31 | const cookies1 = await this.cookieJar.getCookies("https://twitter.com"); 32 | 33 | const critical = ["auth_token", "ct0", "twid"]; 34 | const missing = critical.filter( 35 | (name) => !cookies1.some((c) => c.key === name && c.value) 36 | ); 37 | 38 | if (missing.length) { 39 | throw new Error(`Missing critical cookies: ${missing.join(", ")}`); 40 | } 41 | } 42 | 43 | private constructCookieString(cookie: any): string { 44 | const parts = [ 45 | `${cookie.name}=${cookie.value}`, 46 | `Domain=${cookie.domain}`, 47 | `Path=${cookie.path || "/"}`, 48 | ]; 49 | 50 | if (cookie.expires && cookie.expires !== -1) { 51 | parts.push(`Expires=${new Date(cookie.expires * 1000).toUTCString()}`); 52 | } 53 | 54 | if (cookie.httpOnly) parts.push("HttpOnly"); 55 | if (cookie.secure) parts.push("Secure"); 56 | if (cookie.sameSite) parts.push(`SameSite=${cookie.sameSite}`); 57 | 58 | return parts.join("; "); 59 | } 60 | 61 | async getHeaders(): Promise> { 62 | // Try both domains when getting cookies 63 | let cookieString = ""; 64 | let cookies: Cookie[] = []; 65 | 66 | for (const domain of this.domains) { 67 | const domainCookies = await this.cookieJar.getCookies( 68 | `https://${domain}` 69 | ); 70 | cookies = [...cookies, ...domainCookies]; 71 | } 72 | 73 | cookieString = cookies.map((c) => `${c.key}=${c.value}`).join("; "); 74 | 75 | const csrfToken = cookies.find((c) => c.key === "ct0")?.value; 76 | 77 | const headers: Record = { 78 | authorization: `Bearer ${this.bearerToken}`, 79 | cookie: cookieString, 80 | "content-type": "application/json", 81 | "x-twitter-active-user": "yes", 82 | "x-twitter-auth-type": "OAuth2Session", 83 | "x-twitter-client-language": "en", 84 | "User-Agent": 85 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", 86 | }; 87 | 88 | if (csrfToken) { 89 | headers["x-csrf-token"] = csrfToken; 90 | } 91 | 92 | return headers; 93 | } 94 | 95 | async isAuthenticated(): Promise { 96 | const cookies = await this.cookieJar.getCookies("https://x.com"); 97 | return cookies.some((cookie) => cookie.key === "auth_token"); 98 | } 99 | } 100 | 101 | export class APIv2AuthStrategy implements TwitterAuthStrategy { 102 | private client: TwitterApi; 103 | private bearerToken: string | null = null; 104 | 105 | constructor(credentials: APIv2Credentials) { 106 | this.client = new TwitterApi({ 107 | appKey: credentials.appKey, 108 | appSecret: credentials.appSecret, 109 | accessToken: credentials.accessToken, 110 | accessSecret: credentials.accessSecret, 111 | }); 112 | } 113 | 114 | async getHeaders(): Promise> { 115 | if (!this.bearerToken) { 116 | //this.bearerToken = await this.client.appLogin(); 117 | } 118 | 119 | return { 120 | authorization: `Bearer ${this.bearerToken}`, 121 | "content-type": "application/json", 122 | }; 123 | } 124 | 125 | getClient(): TwitterApi { 126 | return this.client; 127 | } 128 | 129 | async isAuthenticated(): Promise { 130 | try { 131 | await this.client.v2.me(); 132 | return true; 133 | } catch { 134 | return false; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/core/platform/twitter/api/src/client.ts: -------------------------------------------------------------------------------- 1 | import { CookieAuthStrategy } from "./auth/strategies"; 2 | import { 3 | type Profile, 4 | type SearchOptions, 5 | type Tweet, 6 | type TweetOptions, 7 | type UserTimelineOptions, 8 | } from "./interfaces"; 9 | import { GraphQLRequestStrategy } from "./strategies/graphql-request-strategy"; 10 | 11 | export class TwitterClient { 12 | private readonly requestStrategy: GraphQLRequestStrategy; 13 | 14 | private constructor(requestStrategy: GraphQLRequestStrategy) { 15 | this.requestStrategy = requestStrategy; 16 | } 17 | 18 | static async createWithCookies(cookies: string[]): Promise { 19 | const authStrategy = new CookieAuthStrategy(); 20 | await authStrategy.setCookies(cookies); 21 | return new TwitterClient( 22 | new GraphQLRequestStrategy(authStrategy.getHeaders.bind(authStrategy)) 23 | ); 24 | } 25 | 26 | async sendTweet(text: string, options?: TweetOptions): Promise { 27 | const regex = /^(["'])(.*)(["'])$/; 28 | const tweetTextWithoutQuotes = text.replace(regex, "$2"); 29 | const response = await this.requestStrategy.sendTweet( 30 | tweetTextWithoutQuotes, 31 | options 32 | ); 33 | return response.data; 34 | } 35 | 36 | async getTweet(id: string): Promise { 37 | const response = await this.requestStrategy.getTweet(id); 38 | return response.data; 39 | } 40 | 41 | async likeTweet(id: string): Promise { 42 | await this.requestStrategy.likeTweet(id); 43 | } 44 | 45 | async retweet(id: string): Promise { 46 | await this.requestStrategy.retweet(id); 47 | } 48 | 49 | async getProfile(username: string): Promise { 50 | const response = await this.requestStrategy.getProfile(username); 51 | return response.data; 52 | } 53 | 54 | async getUserTimeline( 55 | username: string, 56 | options?: UserTimelineOptions 57 | ): Promise { 58 | const response = await this.requestStrategy.getUserTimeline( 59 | username, 60 | options 61 | ); 62 | return response.data; 63 | } 64 | 65 | async followUser(username: string): Promise { 66 | await this.requestStrategy.followUser(username); 67 | } 68 | 69 | async searchTweets( 70 | query: string, 71 | options?: SearchOptions 72 | ): Promise<{ tweets: Tweet[]; nextCursor?: string }> { 73 | const response = await this.requestStrategy.searchTweets(query, options); 74 | return { 75 | tweets: response.data, 76 | nextCursor: response.meta?.nextCursor, 77 | }; 78 | } 79 | 80 | /* async getMentions( 81 | options?: MentionOptions 82 | ): Promise<{ tweets: Tweet[]; nextCursor?: string }> { 83 | const response = await this.requestStrategy.getMentions(options); 84 | return { 85 | tweets: response.data, 86 | nextCursor: response.meta?.nextCursor, 87 | }; 88 | } */ 89 | } 90 | 91 | export class TwitterClientFactory { 92 | static async createWithCookies(cookies: string[]): Promise { 93 | return TwitterClient.createWithCookies(cookies); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/core/platform/twitter/api/src/error.ts: -------------------------------------------------------------------------------- 1 | // errors.ts 2 | 3 | export class TwitterError extends Error { 4 | constructor( 5 | public readonly code: number, 6 | message: string, 7 | public readonly data?: any 8 | ) { 9 | super(message); 10 | this.name = "TwitterError"; 11 | } 12 | } 13 | 14 | export class TwitterAuthError extends TwitterError { 15 | constructor(message: string, data?: any) { 16 | super(401, message, data); 17 | this.name = "TwitterAuthError"; 18 | } 19 | } 20 | 21 | export class TwitterRateLimitError extends TwitterError { 22 | constructor(message: string, data?: any) { 23 | super(429, message, data); 24 | this.name = "TwitterRateLimitError"; 25 | } 26 | } 27 | 28 | export class TwitterMediaUploadError extends TwitterError { 29 | constructor(message: string, data?: any) { 30 | super(400, message, data); 31 | this.name = "TwitterMediaUploadError"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/core/platform/twitter/api/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Profile { 2 | id: string; 3 | username: string; 4 | name: string; 5 | bio?: string; 6 | metrics?: { 7 | followers: number; 8 | following: number; 9 | tweets: number; 10 | }; 11 | isPrivate: boolean; 12 | isVerified: boolean; 13 | isBlueVerified?: boolean; 14 | avatar?: string; 15 | banner?: string; 16 | location?: string; 17 | website?: string; 18 | joined?: Date; 19 | } 20 | 21 | export interface UserTimelineOptions { 22 | limit?: number; 23 | cursor?: string; 24 | excludeReplies?: boolean; 25 | excludeRetweets?: boolean; 26 | } 27 | 28 | export interface Tweet { 29 | id: string; 30 | text: string; 31 | authorId: string; 32 | authorUsername?: string; 33 | createdAt?: Date; 34 | metrics?: { 35 | likes: number; 36 | retweets: number; 37 | replies: number; 38 | views: number; 39 | bookmarkCount?: number; 40 | }; 41 | media?: { 42 | photos: Photo[]; 43 | videos: Video[]; 44 | }; 45 | quotedTweet?: Tweet; 46 | retweetedTweet?: Tweet; 47 | replyToTweetId?: string; 48 | conversationId?: string; 49 | inReplyToStatus?: Tweet; 50 | isQuoted: boolean; 51 | isReply: boolean; 52 | isRetweet: boolean; 53 | isPin: boolean; 54 | isSelfThread?: boolean; 55 | sensitiveContent?: boolean; 56 | poll?: Poll | null; 57 | isLongform?: boolean; 58 | noteText?: string; 59 | referencedTweets?: { 60 | quoted: string; 61 | replied: string; 62 | retweeted: string; 63 | }; 64 | inReplyToUserId?: string; 65 | inReplyToStatusId?: string; 66 | selfThreadContinuation?: boolean; 67 | } 68 | 69 | export interface Photo { 70 | id: string; 71 | url: string; 72 | altText?: string; 73 | } 74 | 75 | export interface Video { 76 | id: string; 77 | url?: string; 78 | preview: string; 79 | } 80 | 81 | export interface Poll { 82 | id?: string; 83 | options: Array<{ 84 | position?: number; 85 | label: string; 86 | votes?: number; 87 | }>; 88 | end_datetime?: string; 89 | duration_minutes: number; 90 | voting_status?: string; 91 | } 92 | 93 | export interface TweetOptions { 94 | replyToTweet?: string; 95 | quoteTweet?: string; 96 | media?: Array<{ data: Buffer; type: string }>; 97 | poll?: { 98 | options: Array<{ label: string }>; 99 | duration_minutes: number; 100 | }; 101 | longform?: { 102 | richtext?: { 103 | text: string; 104 | }; 105 | languageCode?: string; 106 | }; 107 | } 108 | 109 | export interface SearchOptions { 110 | maxTweets?: number; 111 | cursor?: string; 112 | searchMode?: "Top" | "Latest" | "Photos" | "Videos" | "Users"; 113 | } 114 | 115 | export interface MentionOptions { 116 | count?: number; 117 | cursor?: string; 118 | } 119 | 120 | export interface ITwitterOperations { 121 | sendTweet(text: string, options?: TweetOptions): Promise; 122 | getTweet(id: string): Promise; 123 | likeTweet(id: string): Promise; 124 | retweet(id: string): Promise; 125 | createQuoteTweet( 126 | text: string, 127 | quotedTweetId: string, 128 | options?: Omit 129 | ): Promise; 130 | followUser(username: string): Promise; 131 | searchTweets( 132 | query: string, 133 | options?: SearchOptions 134 | ): Promise<{ tweets: Tweet[]; nextCursor?: string }>; 135 | getProfile(username: string): Promise; 136 | } 137 | 138 | export interface TwitterAuthStrategy { 139 | getHeaders(): Promise>; 140 | isAuthenticated(): Promise; 141 | } 142 | 143 | export interface APIv2Credentials { 144 | appKey: string; 145 | appSecret: string; 146 | accessToken: string; 147 | accessSecret: string; 148 | } 149 | 150 | export interface MediaUploadResult { 151 | mediaId: string; 152 | mediaType: string; 153 | width?: number; 154 | height?: number; 155 | duration?: number; 156 | } 157 | 158 | export interface UserResponse { 159 | data: { 160 | user: { 161 | result: { 162 | rest_id: string; 163 | legacy?: any; 164 | }; 165 | }; 166 | }; 167 | } 168 | -------------------------------------------------------------------------------- /src/core/platform/twitter/api/src/media.ts: -------------------------------------------------------------------------------- 1 | export class MediaUploader { 2 | constructor(private headers: () => Promise>) {} 3 | 4 | async uploadMedia(data: Buffer, mediaType: string) { 5 | const uploadUrl = "https://upload.twitter.com/1.1/media/upload.json"; 6 | const headers = await this.headers(); 7 | 8 | // Remove content-type from headers as FormData will set it 9 | const { "content-type": _, ...uploadHeaders } = headers; 10 | 11 | if (mediaType.startsWith("video/")) { 12 | return this.uploadVideoInChunks( 13 | data, 14 | mediaType, 15 | uploadUrl, 16 | uploadHeaders 17 | ); 18 | } else { 19 | return this.uploadImage(data, mediaType, uploadUrl, uploadHeaders); 20 | } 21 | } 22 | 23 | private async uploadImage( 24 | data: Buffer, 25 | mediaType: string, 26 | uploadUrl: string, 27 | headers: Record 28 | ) { 29 | const form = new FormData(); 30 | form.append("media_category", "tweet_image"); 31 | form.append("media", new Blob([data], { type: mediaType }), "media.jpg"); 32 | 33 | const response = await fetch(uploadUrl, { 34 | method: "POST", 35 | headers: { 36 | ...headers, 37 | }, 38 | body: form, 39 | }); 40 | 41 | if (!response.ok) { 42 | const errorText = await response.text(); 43 | throw new Error(`Failed to upload image: ${errorText}`); 44 | } 45 | 46 | const result = await response.json(); 47 | return { mediaId: result.media_id_string }; 48 | } 49 | 50 | private async uploadVideoInChunks( 51 | data: Buffer, 52 | mediaType: string, 53 | uploadUrl: string, 54 | headers: Record 55 | ) { 56 | // INIT phase 57 | const initForm = new FormData(); 58 | initForm.append("command", "INIT"); 59 | initForm.append("media_type", mediaType); 60 | initForm.append("media_category", "tweet_video"); 61 | initForm.append("total_bytes", data.length.toString()); 62 | 63 | const initResponse = await fetch(uploadUrl, { 64 | method: "POST", 65 | headers: { 66 | ...headers, 67 | }, 68 | body: initForm, 69 | }); 70 | 71 | if (!initResponse.ok) { 72 | const errorText = await initResponse.text(); 73 | console.error("INIT failed:", errorText); 74 | throw new Error(`Failed to initialize video upload: ${errorText}`); 75 | } 76 | 77 | const initData = await initResponse.json(); 78 | const mediaId = initData.media_id_string; 79 | 80 | // APPEND phase 81 | const chunkSize = 5 * 1024 * 1024; // 5MB chunks 82 | let segmentIndex = 0; 83 | const totalChunks = Math.ceil(data.length / chunkSize); 84 | 85 | for (let offset = 0; offset < data.length; offset += chunkSize) { 86 | const chunk = data.slice(offset, offset + chunkSize); 87 | 88 | const appendForm = new FormData(); 89 | appendForm.append("command", "APPEND"); 90 | appendForm.append("media_id", mediaId); 91 | appendForm.append("segment_index", segmentIndex.toString()); 92 | appendForm.append("media", new Blob([chunk], { type: mediaType })); 93 | 94 | const appendResponse = await fetch(uploadUrl, { 95 | method: "POST", 96 | headers: { 97 | ...headers, 98 | }, 99 | body: appendForm, 100 | }); 101 | 102 | if (!appendResponse.ok) { 103 | const errorText = await appendResponse.text(); 104 | console.error("APPEND failed:", errorText); 105 | throw new Error(`Failed to append video chunk: ${errorText}`); 106 | } 107 | 108 | segmentIndex++; 109 | } 110 | 111 | // FINALIZE phase 112 | const finalizeForm = new FormData(); 113 | finalizeForm.append("command", "FINALIZE"); 114 | finalizeForm.append("media_id", mediaId); 115 | 116 | const finalizeResponse = await fetch(uploadUrl, { 117 | method: "POST", 118 | headers: { 119 | ...headers, 120 | }, 121 | body: finalizeForm, 122 | }); 123 | 124 | if (!finalizeResponse.ok) { 125 | const errorText = await finalizeResponse.text(); 126 | console.error("FINALIZE failed:", errorText); 127 | throw new Error(`Failed to finalize video upload: ${errorText}`); 128 | } 129 | 130 | // Check processing status 131 | await this.checkMediaStatus(mediaId, uploadUrl, headers); 132 | 133 | return { mediaId }; 134 | } 135 | 136 | private async checkMediaStatus( 137 | mediaId: string, 138 | uploadUrl: string, 139 | headers: Record 140 | ) { 141 | let processing = true; 142 | while (processing) { 143 | await new Promise((resolve) => setTimeout(resolve, 1000)); 144 | 145 | // Create URL with parameters instead of using FormData 146 | const statusUrl = new URL(uploadUrl); 147 | statusUrl.searchParams.append("command", "STATUS"); 148 | statusUrl.searchParams.append("media_id", mediaId); 149 | 150 | const response = await fetch(statusUrl.toString(), { 151 | method: "GET", 152 | headers: { 153 | ...headers, 154 | "content-type": "application/x-www-form-urlencoded", 155 | }, 156 | }); 157 | 158 | if (!response.ok) { 159 | const errorText = await response.text(); 160 | const headerObj: Record = {}; 161 | response.headers.forEach((value, key) => { 162 | headerObj[key] = value; 163 | }); 164 | 165 | console.error("Status check failed:", { 166 | status: response.status, 167 | statusText: response.statusText, 168 | errorText, 169 | headers: headerObj, 170 | }); 171 | throw new Error(`Failed to check media status: ${errorText}`); 172 | } 173 | 174 | const status = await response.json(); 175 | 176 | if (status.processing_info?.state === "succeeded") { 177 | processing = false; 178 | } else if (status.processing_info?.state === "failed") { 179 | throw new Error( 180 | `Media processing failed: ${JSON.stringify(status.processing_info)}` 181 | ); 182 | } else if (!status.processing_info) { 183 | // If there's no processing_info, assume it's done 184 | processing = false; 185 | } 186 | 187 | // Add timeout to prevent infinite loops 188 | await new Promise((resolve) => setTimeout(resolve, 2000)); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/core/platform/twitter/api/src/operations/base.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type ITwitterOperations, 3 | type Profile, 4 | type SearchOptions, 5 | type Tweet, 6 | type TweetOptions, 7 | type TwitterAuthStrategy, 8 | } from "../interfaces"; 9 | 10 | export abstract class BaseOperations implements ITwitterOperations { 11 | constructor(protected auth: TwitterAuthStrategy) {} 12 | 13 | abstract sendTweet(text: string, options?: TweetOptions): Promise; 14 | abstract getTweet(id: string): Promise; 15 | abstract likeTweet(id: string): Promise; 16 | abstract retweet(id: string): Promise; 17 | abstract createQuoteTweet( 18 | text: string, 19 | quotedTweetId: string, 20 | options?: Omit 21 | ): Promise; 22 | abstract followUser(username: string): Promise; 23 | abstract searchTweets( 24 | query: string, 25 | options?: SearchOptions 26 | ): Promise<{ tweets: Tweet[]; nextCursor?: string }>; 27 | abstract getProfile(username: string): Promise; 28 | 29 | protected async getHeaders(): Promise> { 30 | return this.auth.getHeaders(); 31 | } 32 | 33 | protected async isAuthenticated(): Promise { 34 | return this.auth.isAuthenticated(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/core/platform/twitter/api/src/strategies/request-strategy.ts: -------------------------------------------------------------------------------- 1 | import { TwitterError } from "../error"; 2 | import { 3 | type SearchOptions, 4 | type Tweet, 5 | type TweetOptions, 6 | type UserTimelineOptions, 7 | } from "../interfaces"; 8 | 9 | export interface RequestResponse { 10 | data: T; 11 | meta?: { 12 | nextCursor?: string; 13 | }; 14 | } 15 | 16 | export interface IRequestStrategy { 17 | sendTweet( 18 | text: string, 19 | options?: TweetOptions 20 | ): Promise>; 21 | getTweet(id: string): Promise>; 22 | likeTweet(id: string): Promise>; 23 | retweet(id: string): Promise>; 24 | createQuoteTweet( 25 | text: string, 26 | quotedTweetId: string, 27 | options?: Omit 28 | ): Promise>; 29 | followUser(username: string): Promise>; 30 | searchTweets( 31 | query: string, 32 | options?: SearchOptions 33 | ): Promise>; 34 | getUserTimeline( 35 | username: string, 36 | options?: UserTimelineOptions 37 | ): Promise>; 38 | } 39 | 40 | export abstract class BaseRequestStrategy implements IRequestStrategy { 41 | protected constructor( 42 | protected readonly headers: () => Promise> 43 | ) {} 44 | 45 | abstract sendTweet( 46 | text: string, 47 | options?: TweetOptions 48 | ): Promise>; 49 | abstract getTweet(id: string): Promise>; 50 | abstract likeTweet(id: string): Promise>; 51 | abstract retweet(id: string): Promise>; 52 | abstract createQuoteTweet( 53 | text: string, 54 | quotedTweetId: string, 55 | options?: Omit 56 | ): Promise>; 57 | abstract followUser(username: string): Promise>; 58 | abstract searchTweets( 59 | query: string, 60 | options?: SearchOptions 61 | ): Promise>; 62 | abstract getUserTimeline( 63 | username: string, 64 | options?: UserTimelineOptions 65 | ): Promise>; 66 | protected async makeRequest( 67 | url: string, 68 | method: "GET" | "POST" = "GET", 69 | body?: string | Record, 70 | additionalHeaders?: Record 71 | ): Promise { 72 | const headers = await this.headers(); 73 | const finalHeaders = { 74 | ...headers, 75 | ...additionalHeaders, 76 | }; 77 | const requestInit: RequestInit = { 78 | method, 79 | headers: finalHeaders, 80 | credentials: "include", 81 | }; 82 | 83 | // Only add body for non-GET requests 84 | if (method !== "GET" && body) { 85 | requestInit.body = JSON.stringify(body); 86 | } 87 | 88 | const response = await fetch(url, requestInit); 89 | 90 | const responseText = await response.text(); 91 | let responseData; 92 | 93 | try { 94 | responseData = JSON.parse(responseText); 95 | } catch (e) { 96 | responseData = responseText; 97 | } 98 | 99 | if (!response.ok) { 100 | if (responseData?.errors?.[0]?.message) { 101 | throw new TwitterError( 102 | response.status, 103 | responseData.errors[0].message, 104 | responseData 105 | ); 106 | } 107 | throw new TwitterError(response.status, "Request failed", responseData); 108 | } 109 | 110 | return responseData; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/core/services/Event.ts: -------------------------------------------------------------------------------- 1 | import { dbSchemas } from "@/db"; 2 | import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; 3 | import type { CharacterManager } from "../managers/character"; 4 | import type { InteractionEventPayload, InteractionEventType } from "../types"; 5 | 6 | export class EventService { 7 | constructor( 8 | private db: PostgresJsDatabase, 9 | private characterManager: CharacterManager 10 | ) {} 11 | 12 | async createEvents( 13 | events: Array<{ 14 | characterId: string; 15 | type: InteractionEventType; // Updated type 16 | payload: Record; 17 | metadata?: Record; 18 | }> 19 | ) { 20 | return this.db 21 | .insert(dbSchemas.events) 22 | .values( 23 | events.map((event) => ({ 24 | characterId: event.characterId, 25 | type: event.type, 26 | payload: event.payload, 27 | metadata: { 28 | ...event.metadata, 29 | timestamp: new Date().toISOString(), 30 | source: "system", 31 | }, 32 | processed: false, 33 | })) 34 | ) 35 | .returning(); 36 | } 37 | 38 | async createInteractionEvent( 39 | type: T, 40 | payload: InteractionEventPayload[T] 41 | ) { 42 | const character = await this.characterManager.getCharacter(); 43 | try { 44 | const result = await this.db 45 | .insert(dbSchemas.events) 46 | .values({ 47 | characterId: character?.id, 48 | type, 49 | payload, 50 | metadata: { 51 | timestamp: new Date().toISOString(), 52 | source: "interaction-service", 53 | }, 54 | processed: false, 55 | }) 56 | .returning(); 57 | 58 | return result[0]; 59 | } catch (error) { 60 | console.error("Error creating interaction event:", { 61 | type, 62 | characterId: character.id, 63 | error: 64 | error instanceof Error 65 | ? { 66 | message: error.message, 67 | name: error.name, 68 | code: (error as any).code, 69 | } 70 | : error, 71 | }); 72 | throw error; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/core/services/quantum-state.ts: -------------------------------------------------------------------------------- 1 | import { log } from "@/core/utils/logger"; 2 | import EventEmitter from "events"; 3 | import cron from "node-cron"; 4 | import type { QuantumStateManager } from "../managers/quantum"; 5 | import type { EventService } from "../services/Event"; 6 | 7 | export interface StateUpdateConfig { 8 | enabled: boolean; 9 | cronSchedule?: string; // Default to hourly 10 | maxRetries?: number; 11 | initialDelayMs?: number; 12 | checkInitialState?: boolean; // Default to true 13 | } 14 | 15 | export class StateUpdateService extends EventEmitter { 16 | private cronJob?: cron.ScheduledTask; 17 | private retryCount: number = 0; 18 | private isUpdating: boolean = false; 19 | 20 | constructor( 21 | private quantumManager: QuantumStateManager, 22 | private eventService: EventService, 23 | private config: StateUpdateConfig 24 | ) { 25 | super(); 26 | this.config = { 27 | cronSchedule: "0 * * * *", // Every hour by default 28 | maxRetries: 0, 29 | initialDelayMs: 1000, 30 | checkInitialState: true, // Default to true 31 | ...config, 32 | }; 33 | } 34 | 35 | async start(): Promise { 36 | if (!this.config.enabled) { 37 | log.info("State update service disabled, skipping initialization"); 38 | return; 39 | } 40 | 41 | log.info("Starting state update service..."); 42 | 43 | // Check for existing states if enabled 44 | if (this.config.checkInitialState) { 45 | try { 46 | const latestState = await this.quantumManager.getLatestState(); 47 | if (!latestState) { 48 | log.info( 49 | "No existing quantum states found, generating initial state..." 50 | ); 51 | // Immediate generation for first state 52 | await this.performUpdate(); 53 | } else { 54 | log.info( 55 | "Found existing quantum state, continuing with regular schedule" 56 | ); 57 | // No need to perform immediate update if state exists 58 | } 59 | } catch (error) { 60 | log.error("Error checking initial state:", error); 61 | } 62 | } 63 | 64 | // Set up cron job 65 | this.cronJob = cron.schedule(this.config.cronSchedule!, async () => { 66 | await this.performUpdate(); 67 | }); 68 | 69 | await this.eventService.createInteractionEvent("interaction.started", { 70 | input: "Quantum state update service started", 71 | responseType: "text", 72 | platform: "api", 73 | timestamp: new Date().toISOString(), 74 | messageId: `quantum-state-${Date.now()}`, 75 | user: { 76 | id: "system", 77 | username: "system", 78 | }, 79 | }); 80 | 81 | this.emit("started"); 82 | } 83 | 84 | async stop(): Promise { 85 | log.info("Stopping state update service..."); 86 | if (this.cronJob) { 87 | this.cronJob.stop(); 88 | this.cronJob = undefined; 89 | } 90 | 91 | await this.eventService.createInteractionEvent("interaction.completed", { 92 | input: "Quantum state update service stopped", 93 | response: "Service stopped successfully", 94 | responseType: "text", 95 | platform: "api", 96 | timestamp: new Date().toISOString(), 97 | messageId: `quantum-state-${Date.now()}`, 98 | processingTime: 0, 99 | user: { 100 | id: "system", 101 | username: "system", 102 | }, 103 | }); 104 | 105 | this.emit("stopped"); 106 | } 107 | 108 | private async performUpdate(): Promise { 109 | if (this.isUpdating) { 110 | log.warn("Update already in progress, skipping..."); 111 | return; 112 | } 113 | 114 | this.isUpdating = true; 115 | 116 | try { 117 | log.info("Generating new quantum state..."); 118 | 119 | await this.eventService.createInteractionEvent("interaction.started", { 120 | input: "Generating new quantum state", 121 | responseType: "text", 122 | platform: "api", 123 | timestamp: new Date().toISOString(), 124 | messageId: `quantum-state-${Date.now()}`, 125 | user: { 126 | id: "system", 127 | username: "system", 128 | }, 129 | }); 130 | 131 | const newState = await this.quantumManager.generateQuantumState(); 132 | const storedState = await this.quantumManager.storeState(newState); 133 | 134 | await this.eventService.createInteractionEvent("interaction.completed", { 135 | input: "New quantum state generated and stored", 136 | response: "State updated successfully", 137 | responseType: "text", 138 | platform: "api", 139 | timestamp: new Date().toISOString(), 140 | messageId: `quantum-state-${Date.now()}`, 141 | user: { 142 | id: "system", 143 | username: "system", 144 | }, 145 | processingTime: 0, 146 | }); 147 | 148 | log.info("New quantum state generated and stored successfully"); 149 | this.emit("stateUpdated", storedState); 150 | 151 | // Reset retry count on success 152 | this.retryCount = 0; 153 | } catch (error) { 154 | log.error("Failed to update quantum state:", error); 155 | 156 | await this.eventService.createInteractionEvent("interaction.failed", { 157 | input: "Failed to update quantum state", 158 | error: error instanceof Error ? error.message : "Unknown error", 159 | errorCode: "quantum-state-update-failed", 160 | timestamp: new Date().toISOString(), 161 | messageId: `quantum-state-${Date.now()}`, 162 | user: { 163 | id: "system", 164 | username: "system", 165 | }, 166 | }); 167 | 168 | this.emit("updateError", error); 169 | 170 | if (this.retryCount < (this.config.maxRetries || 0)) { 171 | this.retryCount++; 172 | const backoffMs = Math.min( 173 | 1000 * Math.pow(2, this.retryCount), 174 | 3600000 // Max 1 hour 175 | ); 176 | log.info(`Retrying in ${backoffMs}ms (attempt ${this.retryCount})...`); 177 | setTimeout(() => this.performUpdate(), backoffMs); 178 | return; 179 | } 180 | } finally { 181 | this.isUpdating = false; 182 | } 183 | } 184 | 185 | // Manual trigger for testing or forced updates 186 | async triggerUpdate(): Promise { 187 | await this.performUpdate(); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/core/utils/IBMRest.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosInstance } from "axios"; 2 | import { log } from "./logger"; 3 | 4 | export interface IBMConfig { 5 | apiToken: string; 6 | backend?: string; 7 | timeout?: number; 8 | maxQueueTime?: number; // Maximum time to wait in queue (ms) 9 | maxRunTime?: number; // Maximum time to wait for execution (ms) 10 | } 11 | 12 | export interface JobResult { 13 | meas: { 14 | get_counts: () => Record; 15 | }; 16 | metadata: { 17 | execution_time: number; 18 | backend: string; 19 | }; 20 | } 21 | 22 | interface JobResponse { 23 | id: string; 24 | state: { 25 | status: "Queued" | "Running" | "Completed" | "Failed"; 26 | reason?: string; 27 | }; 28 | results?: JobResult[]; 29 | } 30 | 31 | export class IBMQuantumClient { 32 | private client: AxiosInstance; 33 | private readonly config: Required; 34 | 35 | constructor(userConfig: IBMConfig) { 36 | this.config = { 37 | apiToken: userConfig.apiToken, 38 | backend: userConfig.backend ?? "ibm_brisbane", 39 | timeout: userConfig.timeout ?? 30000, 40 | maxQueueTime: userConfig.maxQueueTime ?? 300000, // 5 minutes default queue wait 41 | maxRunTime: userConfig.maxRunTime ?? 240000, // 4 minutes default run wait 42 | }; 43 | 44 | this.client = axios.create({ 45 | baseURL: "https://api.quantum-computing.ibm.com/runtime", 46 | timeout: this.config.timeout, 47 | headers: { 48 | accept: "application/json", 49 | Authorization: `Bearer ${this.config.apiToken}`, 50 | "Content-Type": "application/json", 51 | }, 52 | }); 53 | } 54 | 55 | async submitJob(circuitQASM: string): Promise { 56 | try { 57 | log.info(`Submitting circuit to backend: ${this.config.backend}`); 58 | const validatedCircuit = await this.validateCircuit(circuitQASM); 59 | //log.info(`Validated circuit: ${validatedCircuit}`); 60 | 61 | const response = await this.client.post("/jobs", { 62 | program_id: "sampler", 63 | backend: this.config.backend, 64 | hub: "ibm-q", 65 | group: "open", 66 | project: "main", 67 | params: { 68 | pubs: [[validatedCircuit]], 69 | shots: 1024, 70 | version: 2, 71 | }, 72 | }); 73 | 74 | const jobId = response.data.id; 75 | log.info(`Job submitted with ID: ${jobId}`); 76 | 77 | // First wait for job to start executing 78 | await this.waitForJobStart(jobId); 79 | 80 | // Then wait for completion and get results 81 | return await this.waitForJobResults(jobId); 82 | } catch (error: any) { 83 | if (error.response) { 84 | log.error("Error response:", { 85 | status: error.response.status, 86 | data: error.response.data, 87 | }); 88 | } 89 | 90 | const errorMessage = 91 | error.response?.data?.error?.message || 92 | error.message || 93 | "Unknown error"; 94 | log.error(`Failed to submit job: ${errorMessage}`); 95 | throw new Error(`Failed to submit job: ${errorMessage}`); 96 | } 97 | } 98 | 99 | private async getJobStatus(jobId: string): Promise { 100 | const response = await this.client.get(`/jobs/${jobId}`); 101 | return response.data; 102 | } 103 | 104 | private async waitForJobStart(jobId: string): Promise { 105 | const startTime = Date.now(); 106 | const pollInterval = 2000; // 2 seconds 107 | 108 | while (true) { 109 | const status = await this.getJobStatus(jobId); 110 | 111 | if (status.state.status === "Running") { 112 | log.info(`Job ${jobId} is now running`); 113 | return; 114 | } 115 | 116 | if (status.state.status === "Failed") { 117 | throw new Error(`Job ${jobId} failed: ${status.state.reason}`); 118 | } 119 | 120 | if (Date.now() - startTime > this.config.maxQueueTime) { 121 | throw new Error( 122 | `Job ${jobId} exceeded maximum queue time of ${this.config.maxQueueTime}ms` 123 | ); 124 | } 125 | 126 | if (status.state.status !== "Queued") { 127 | throw new Error(`Unexpected job status: ${status.state.status}`); 128 | } 129 | 130 | await new Promise((resolve) => setTimeout(resolve, pollInterval)); 131 | } 132 | } 133 | 134 | private async waitForJobResults(jobId: string): Promise { 135 | const startTime = Date.now(); 136 | const pollInterval = 1000; 137 | 138 | while (true) { 139 | const status = await this.getJobStatus(jobId); 140 | 141 | if (status.state.status === "Completed") { 142 | const response = await this.client.get(`/jobs/${jobId}/results`); 143 | 144 | // Log raw results to see what we're getting 145 | console.log("Raw quantum results:", response.data); 146 | 147 | const samples = response.data.results[0].data.c.samples; 148 | if (!samples || !Array.isArray(samples)) { 149 | throw new Error("No valid samples in job results"); 150 | } 151 | 152 | // Modified conversion to ensure we're processing bits correctly 153 | const counts: Record = samples.reduce( 154 | (acc: Record, curr: string | number) => { 155 | // Convert the hex string to binary, ensuring proper handling 156 | const hexValue = 157 | typeof curr === "string" 158 | ? curr.replace("0x", "") 159 | : curr.toString(16); 160 | const binary = BigInt(`0x${hexValue}`) 161 | .toString(2) 162 | .padStart(16, "0"); 163 | acc[binary] = (acc[binary] || 0) + 1; 164 | return acc; 165 | }, 166 | {} 167 | ); 168 | 169 | return { 170 | meas: { 171 | get_counts: () => counts, 172 | }, 173 | metadata: { 174 | execution_time: response.data.metadata?.execution 175 | ?.execution_spans?.[0]?.[1]?.date 176 | ? new Date( 177 | response.data.metadata.execution.execution_spans[0][1].date 178 | ).getTime() - 179 | new Date( 180 | response.data.metadata.execution.execution_spans[0][0].date 181 | ).getTime() 182 | : 0, 183 | backend: this.config.backend, 184 | }, 185 | }; 186 | } 187 | 188 | if (status.state.status === "Failed") { 189 | throw new Error(`Job ${jobId} failed: ${status.state.reason}`); 190 | } 191 | 192 | if (Date.now() - startTime > this.config.maxRunTime) { 193 | throw new Error( 194 | `Job ${jobId} exceeded maximum execution time of ${this.config.maxRunTime}ms` 195 | ); 196 | } 197 | 198 | if (status.state.status !== "Running") { 199 | throw new Error(`Unexpected job status: ${status.state.status}`); 200 | } 201 | 202 | await new Promise((resolve) => setTimeout(resolve, pollInterval)); 203 | log.info(`Job ${jobId} still running...`); 204 | } 205 | } 206 | 207 | // Modified generateCircuitQASM function for more quantum variation 208 | generateCircuitQASM(numQubits: number = 16): string { 209 | const lines = ["OPENQASM 2.0;", 'include "qelib1.inc";']; 210 | 211 | // Registers 212 | lines.push(`qreg q[${numQubits}];`); 213 | lines.push(`creg c[${numQubits}];`); 214 | 215 | // Create more varied superposition states 216 | for (let i = 0; i < numQubits; i++) { 217 | // More extreme rotation angles to create bigger differences 218 | lines.push(`sx q[${i}];`); 219 | // Use different rotation patterns for first/second half of qubits 220 | if (i < numQubits / 2) { 221 | lines.push(`rz(pi/2) q[${i}];`); // Stronger rotation 222 | } else { 223 | lines.push(`rz(pi/8) q[${i}];`); // Weaker rotation 224 | } 225 | // Second superposition with different angle 226 | lines.push(`sx q[${i}];`); 227 | } 228 | 229 | // Measure 230 | for (let i = 0; i < numQubits; i++) { 231 | lines.push(`measure q[${i}] -> c[${i}];`); 232 | } 233 | 234 | return lines.join("\n"); 235 | } 236 | 237 | // Also update validateCircuit to use a similar fallback circuit 238 | private async validateCircuit(circuitQASM: string): Promise { 239 | try { 240 | return circuitQASM; 241 | } catch (error) { 242 | log.warn("Complex circuit failed, falling back to basic measurements"); 243 | const lines = ["OPENQASM 2.0;", 'include "qelib1.inc";']; 244 | 245 | lines.push(`qreg q[16];`); 246 | lines.push(`creg c[16];`); 247 | 248 | // More varied fallback circuit 249 | for (let i = 0; i < 16; i++) { 250 | lines.push(`sx q[${i}];`); 251 | lines.push(`rz(pi/${2 + (i % 4)}) q[${i}];`); 252 | } 253 | 254 | // Measure 255 | for (let i = 0; i < 16; i++) { 256 | lines.push(`measure q[${i}] -> c[${i}];`); 257 | } 258 | 259 | return lines.join("\n"); 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/core/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | export const log = { 4 | info: (message: string, data?: any) => { 5 | console.log( 6 | chalk.blue("ℹ"), 7 | chalk.blue(message), 8 | data ? chalk.gray(JSON.stringify(data)) : "" 9 | ); 10 | }, 11 | sent: (message: string, data?: any) => { 12 | console.log( 13 | chalk.green("⚡"), 14 | chalk.green(message), 15 | data ? chalk.gray(JSON.stringify(data)) : "" 16 | ); 17 | }, 18 | receiving: (message: string, data?: any) => { 19 | console.log( 20 | chalk.yellow("✉️"), 21 | chalk.yellow(message), 22 | data ? chalk.gray(JSON.stringify(data)) : "" 23 | ); 24 | }, 25 | error: (message: string, error?: any) => { 26 | console.error( 27 | chalk.red("✖"), 28 | chalk.red(message), 29 | error ? chalk.gray(error.message || JSON.stringify(error)) : "" 30 | ); 31 | }, 32 | debug: (message: string, data?: any) => { 33 | console.debug( 34 | chalk.gray("🐛"), 35 | chalk.gray(message), 36 | data ? chalk.gray(JSON.stringify(data)) : "" 37 | ); 38 | }, 39 | warn: (message: string, data?: any) => { 40 | console.warn( 41 | chalk.yellow("⚠️"), 42 | chalk.yellow(message), 43 | data ? chalk.gray(JSON.stringify(data)) : "" 44 | ); 45 | }, 46 | network: (message: string, data?: any) => { 47 | console.log( 48 | chalk.magenta("⚡"), 49 | chalk.magenta(message), 50 | data ? chalk.gray(JSON.stringify(data)) : "" 51 | ); 52 | }, 53 | message: (message: string, data?: any) => { 54 | console.log( 55 | chalk.cyan("💬"), 56 | chalk.cyan(message), 57 | data ? chalk.gray(JSON.stringify(data)) : "" 58 | ); 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /src/core/utils/utils.ts: -------------------------------------------------------------------------------- 1 | export const formatToolData = (data: any, depth = 0): string => { 2 | if (!data) return ""; 3 | 4 | let context = ""; 5 | 6 | if (typeof data !== "object") { 7 | return String(data); 8 | } 9 | 10 | Object.entries(data).forEach(([key, value]) => { 11 | const formattedKey = key 12 | .replace(/([A-Z])/g, " $1") 13 | .replace(/^./, (str) => str.toUpperCase()); 14 | 15 | if (typeof value === "object") { 16 | context += `${formattedKey}:\n${formatToolData(value, depth + 1)}\n`; 17 | } else { 18 | let formattedValue = value; 19 | if (typeof value === "number") { 20 | formattedValue = Number.isInteger(value) ? value : value.toFixed(2); 21 | } 22 | context += `${formattedKey}: ${formattedValue}\n`; 23 | } 24 | }); 25 | 26 | return context; 27 | }; 28 | -------------------------------------------------------------------------------- /src/create-character/base.ts: -------------------------------------------------------------------------------- 1 | export const CHARACTER_SCHEMA_PROMPT = ` 2 | You will analyze data to create a character profile. 3 | Output must be valid JSON matching this exact structure (all fields required): 4 | 5 | { 6 | "name": string, 7 | "bio": string, 8 | "personalityTraits": string[], 9 | "responseStyles": { 10 | "default": { 11 | "tone": string[], 12 | "personality": string[], 13 | "guidelines": string[] 14 | }, 15 | "platforms": Record 33 | }> 34 | }, 35 | "styles": Record, 39 | "shouldRespond": { 40 | "rules": string[], 41 | "examples": string[] 42 | }, 43 | "hobbies": Array<{ 44 | "name": string, 45 | "proficiency"?: number, 46 | "lastPracticed"?: string, 47 | "relatedTopics"?: string[] 48 | }>, 49 | "beliefSystem": string[], 50 | "preferences": { 51 | "preferredTopics": string[], 52 | "dislikedTopics": string[], 53 | "preferredTimes": string[], 54 | "dislikedTimes": string[], 55 | "preferredDays": string[], 56 | "dislikedDays": string[], 57 | "preferredHours": string[], 58 | "dislikedHours": string[], 59 | "generalLikes": string[], 60 | "generalDislikes": string[] 61 | }, 62 | "goals": Array<{ 63 | "description": string, 64 | "status": "active" | "completed" | "paused", 65 | "progress": number, 66 | "metadata": { 67 | "dependencies": string[], 68 | "completionCriteria": string[], 69 | "notes": string[] 70 | } 71 | }> 72 | } 73 | 74 | Each field must be populated based on observable data, not assumptions. 75 | Include confidence scores (0-1) for each major field in metadata 76 | `; 77 | 78 | // prompts/character/chat.ts 79 | export const CHAT_TO_CHARACTER_PROMPT = ` 80 | 81 | Analyze these chat messages to create a complete character profile. 82 | Messages will show consistent patterns in: 83 | 1. Communication style and tone 84 | 2. Response patterns and timing 85 | 3. Topic preferences and expertise 86 | 4. Expression and emotion 87 | 5. Social interaction patterns 88 | 6. Platform-specific behaviors 89 | 90 | Chat History: 91 | 92 | 93 | ${CHARACTER_SCHEMA_PROMPT} 94 | `; 95 | 96 | // prompts/character/tweet.ts 97 | export const TWEET_TO_CHARACTER_PROMPT = `Analyze these tweets to create a complete character profile. 98 | Focus on identifying: 99 | 1. Writing style and voice 100 | 2. Platform behaviors (hashtags, mentions, threads) 101 | 3. Engagement patterns 102 | 4. Content preferences 103 | 5. Temporal patterns 104 | 6. Social dynamics 105 | 106 | Tweet History: 107 | 108 | 109 | ${CHARACTER_SCHEMA_PROMPT}`; 110 | 111 | export const MERGE_PROFILES_PROMPT = `Merge these character profiles into a single consistent profile. 112 | Merging rules: 113 | 1. Prefer higher confidence data points 114 | 2. Maintain consistency across all fields 115 | 3. Combine unique traits/preferences 116 | 4. Preserve platform-specific behaviors 117 | 5. Weight recent data more heavily 118 | 6. Keep all required fields populated 119 | 120 | Input Profiles: 121 | 122 | 123 | ${CHARACTER_SCHEMA_PROMPT}`; 124 | -------------------------------------------------------------------------------- /src/create-character/types.ts: -------------------------------------------------------------------------------- 1 | import type { CreateCharacterInput } from "@/types"; 2 | 3 | export interface AnalysisSource { 4 | type: "chat" | "tweet" | "discord"; 5 | data: any; 6 | metadata?: { 7 | platform?: string; 8 | timeframe?: { 9 | start: Date; 10 | end: Date; 11 | }; 12 | messageCount?: number; 13 | confidence?: number; 14 | }; 15 | } 16 | 17 | export interface AnalysisResult { 18 | profile: CreateCharacterInput; 19 | metadata: { 20 | source: AnalysisSource; 21 | confidence: { 22 | overall: number; 23 | traits: number; 24 | preferences: number; 25 | styles: number; 26 | }; 27 | timestamp: string; 28 | }; 29 | } 30 | 31 | export interface ChatMessage { 32 | senderId: string; 33 | senderName: string; 34 | content: string; 35 | timestamp: Date; 36 | threadId?: string; 37 | replyTo?: string; 38 | platform?: string; 39 | metadata?: { 40 | reactions?: Array<{ 41 | type: string; 42 | count: number; 43 | users: string[]; 44 | }>; 45 | mentions?: string[]; 46 | attachments?: Array<{ 47 | type: string; 48 | url: string; 49 | name: string; 50 | }>; 51 | importance?: number; 52 | tags?: string[]; 53 | isEdited?: boolean; 54 | originalContent?: string; 55 | editHistory?: Array<{ 56 | content: string; 57 | timestamp: Date; 58 | }>; 59 | }; 60 | } 61 | 62 | export interface Tweet { 63 | id: string; 64 | text: string; 65 | created_at: string; 66 | retweet_count: number; 67 | favorite_count: number; 68 | reply_count: number; 69 | user: { 70 | id: string; 71 | screen_name: string; 72 | name: string; 73 | }; 74 | in_reply_to_status_id?: string; 75 | in_reply_to_user_id?: string; 76 | quoted_status_id?: string; 77 | is_quote_status: boolean; 78 | entities?: { 79 | hashtags: Array<{ text: string }>; 80 | user_mentions: Array<{ screen_name: string; id: string }>; 81 | urls: Array<{ url: string; expanded_url: string }>; 82 | }; 83 | metadata?: { 84 | iso_language_code: string; 85 | result_type: string; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import * as schema from "@/db/schema/schema"; 2 | import dotenv from "dotenv"; 3 | import { drizzle } from "drizzle-orm/postgres-js"; 4 | import postgres from "postgres"; 5 | import { goals } from "./schema/goal"; 6 | 7 | dotenv.config(); 8 | 9 | const connectionString = process.env.DATABASE_URL; 10 | 11 | if (!connectionString) { 12 | throw new Error("DATABASE_URL environment variable is not set"); 13 | } 14 | 15 | export const dbSchemas = { 16 | ...schema, 17 | goals: goals, 18 | } as const; 19 | 20 | // Type the combined schema 21 | 22 | // Create the connection 23 | const client = postgres(connectionString, { 24 | max: 20, // maximum number of connections 25 | idle_timeout: 20, // how long a connection can be idle before being closed 26 | connect_timeout: 10, // how long to wait for a connection 27 | debug: (msg) => {}, 28 | }); 29 | export const db = drizzle(client, { schema: dbSchemas }); 30 | 31 | // Export a function to test the connection 32 | export async function testConnection() { 33 | try { 34 | // Try a simple query 35 | const result = await db.select().from(schema.characters).limit(1); 36 | console.log("Database connection successful"); 37 | return true; 38 | } catch (error) { 39 | console.error("Database connection failed:", error); 40 | return false; 41 | } 42 | } 43 | 44 | export const pgClient = client; 45 | 46 | export { schema }; 47 | -------------------------------------------------------------------------------- /src/db/migrate.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import { drizzle } from "drizzle-orm/postgres-js"; 3 | import { migrate } from "drizzle-orm/postgres-js/migrator"; 4 | import postgres from "postgres"; 5 | 6 | dotenv.config(); 7 | const runMigrate = async () => { 8 | if (!process.env.DATABASE_URL) { 9 | throw new Error("DATABASE_URL is not defined"); 10 | } 11 | 12 | const connection = postgres(process.env.DATABASE_URL, { 13 | max: 1, 14 | debug: true, 15 | }); 16 | 17 | const db = drizzle(connection); 18 | 19 | console.log("⏳ Running migrations..."); 20 | 21 | const start = Date.now(); 22 | 23 | await migrate(db, { migrationsFolder: "migrations" }); 24 | 25 | const end = Date.now(); 26 | 27 | console.log("✅ Migrations completed in", end - start, "ms"); 28 | 29 | process.exit(0); 30 | }; 31 | 32 | runMigrate().catch((err) => { 33 | console.error("❌ Migration failed"); 34 | console.error(err); 35 | process.exit(1); 36 | }); 37 | -------------------------------------------------------------------------------- /src/db/migrations/0000_orange_living_tribunal.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."conversation_style" AS ENUM('chat', 'post', 'friend', 'professional', 'casual', 'news', 'academic', 'technical', 'creative', 'formal', 'informal', 'adversarial', 'harsh');--> statement-breakpoint 2 | CREATE TYPE "public"."memory_type" AS ENUM('interaction', 'learning', 'achievement', 'hobby');--> statement-breakpoint 3 | CREATE TYPE "public"."platform" AS ENUM('twitter', 'discord', 'telegram', 'slack');--> statement-breakpoint 4 | CREATE TYPE "public"."relationship_status" AS ENUM('friend', 'blocked', 'preferred', 'disliked', 'neutral');--> statement-breakpoint 5 | CREATE TYPE "public"."response_type" AS ENUM('tweet_create', 'tweet_reply', 'tweet_thread', 'discord_chat', 'discord_mod', 'discord_help', 'discord_welcome', 'telegram_chat', 'telegram_group', 'telegram_broadcast', 'slack_chat', 'slack_thread', 'slack_channel', 'slack_dm');--> statement-breakpoint 6 | CREATE TABLE IF NOT EXISTS "characters" ( 7 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 8 | "name" varchar(255) NOT NULL, 9 | "bio" text NOT NULL, 10 | "personality_traits" jsonb NOT NULL, 11 | "general_guidelines" jsonb DEFAULT '[]'::jsonb, 12 | "response_styles" jsonb DEFAULT '{"default":{"tone":[],"personality":[],"guidelines":[]},"platforms":{}}'::jsonb NOT NULL, 13 | "styles" jsonb DEFAULT '{"chat":{"rules":[],"examples":[]},"professional":{"rules":[],"examples":[]},"casual":{"rules":[],"examples":[]}}'::jsonb, 14 | "should_respond" jsonb, 15 | "hobbies" jsonb DEFAULT '[]'::jsonb, 16 | "belief_system" jsonb DEFAULT '[]'::jsonb, 17 | "preferences" jsonb DEFAULT '{"preferredTopics":[],"dislikedTopics":[],"preferredTimes":[],"dislikedTimes":[],"preferredDays":[],"dislikedDays":[],"preferredHours":[],"dislikedHours":[],"generalLikes":[],"generalDislikes":[]}'::jsonb, 18 | "created_at" timestamp DEFAULT now() NOT NULL, 19 | "updated_at" timestamp DEFAULT now() NOT NULL 20 | ); 21 | --> statement-breakpoint 22 | CREATE TABLE IF NOT EXISTS "events" ( 23 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 24 | "character_id" uuid NOT NULL, 25 | "type" varchar(255) NOT NULL, 26 | "payload" jsonb NOT NULL, 27 | "metadata" jsonb NOT NULL, 28 | "processed" boolean DEFAULT false NOT NULL, 29 | "created_at" timestamp DEFAULT now() NOT NULL 30 | ); 31 | --> statement-breakpoint 32 | CREATE TABLE IF NOT EXISTS "goals" ( 33 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 34 | "character_id" uuid NOT NULL, 35 | "description" text NOT NULL, 36 | "status" varchar(50) DEFAULT 'active' NOT NULL, 37 | "progress" numeric DEFAULT '0' NOT NULL, 38 | "metadata" jsonb DEFAULT '{}'::jsonb, 39 | "created_at" timestamp DEFAULT now() NOT NULL, 40 | "completed_at" timestamp, 41 | "updated_at" timestamp DEFAULT now() NOT NULL 42 | ); 43 | --> statement-breakpoint 44 | CREATE TABLE IF NOT EXISTS "memories" ( 45 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 46 | "character_id" uuid NOT NULL, 47 | "type" "memory_type" NOT NULL, 48 | "content" text NOT NULL, 49 | "importance" numeric DEFAULT '0.5' NOT NULL, 50 | "metadata" jsonb DEFAULT '{}'::jsonb, 51 | "created_at" timestamp DEFAULT now() NOT NULL 52 | ); 53 | --> statement-breakpoint 54 | CREATE TABLE IF NOT EXISTS "social_relations" ( 55 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 56 | "character_id" uuid NOT NULL, 57 | "user_id" varchar(255) NOT NULL, 58 | "status" "relationship_status" DEFAULT 'neutral' NOT NULL, 59 | "interaction_count" numeric DEFAULT '0' NOT NULL, 60 | "sentiment" numeric DEFAULT '0' NOT NULL, 61 | "last_interaction" timestamp DEFAULT now() NOT NULL, 62 | "metadata" jsonb DEFAULT '{}'::jsonb, 63 | "created_at" timestamp DEFAULT now() NOT NULL, 64 | "updated_at" timestamp DEFAULT now() NOT NULL 65 | ); 66 | --> statement-breakpoint 67 | DO $$ BEGIN 68 | ALTER TABLE "events" ADD CONSTRAINT "events_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 69 | EXCEPTION 70 | WHEN duplicate_object THEN null; 71 | END $$; 72 | --> statement-breakpoint 73 | DO $$ BEGIN 74 | ALTER TABLE "goals" ADD CONSTRAINT "goals_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 75 | EXCEPTION 76 | WHEN duplicate_object THEN null; 77 | END $$; 78 | --> statement-breakpoint 79 | DO $$ BEGIN 80 | ALTER TABLE "memories" ADD CONSTRAINT "memories_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 81 | EXCEPTION 82 | WHEN duplicate_object THEN null; 83 | END $$; 84 | --> statement-breakpoint 85 | DO $$ BEGIN 86 | ALTER TABLE "social_relations" ADD CONSTRAINT "social_relations_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 87 | EXCEPTION 88 | WHEN duplicate_object THEN null; 89 | END $$; 90 | -------------------------------------------------------------------------------- /src/db/migrations/0001_small_cerise.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "characters" ADD COLUMN "onchain" jsonb DEFAULT '[]'::jsonb; -------------------------------------------------------------------------------- /src/db/migrations/0002_sturdy_spirit.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "characters" ADD COLUMN "identity" jsonb; -------------------------------------------------------------------------------- /src/db/migrations/0003_mature_phil_sheldon.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "characters" CASCADE;--> statement-breakpoint 2 | DROP TABLE "events" CASCADE;--> statement-breakpoint 3 | DROP TABLE "goals" CASCADE;--> statement-breakpoint 4 | DROP TABLE "memories" CASCADE;--> statement-breakpoint 5 | DROP TABLE "social_relations" CASCADE;--> statement-breakpoint 6 | DROP TYPE "public"."conversation_style";--> statement-breakpoint 7 | DROP TYPE "public"."memory_type";--> statement-breakpoint 8 | DROP TYPE "public"."platform";--> statement-breakpoint 9 | DROP TYPE "public"."relationship_status";--> statement-breakpoint 10 | DROP TYPE "public"."response_type"; -------------------------------------------------------------------------------- /src/db/migrations/0004_numerous_storm.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."sentiment" AS ENUM('formal', 'casual', 'friendly', 'professional', 'excited', 'reserved', 'degen', 'curt', 'neutral');--> statement-breakpoint 2 | CREATE TYPE "public"."conversation_style" AS ENUM('chat', 'post', 'friend', 'professional', 'casual', 'news', 'academic', 'technical', 'creative', 'formal', 'informal', 'adversarial', 'harsh');--> statement-breakpoint 3 | CREATE TYPE "public"."memory_type" AS ENUM('interaction', 'learning', 'achievement', 'hobby');--> statement-breakpoint 4 | CREATE TYPE "public"."platform" AS ENUM('twitter', 'discord', 'telegram', 'slack');--> statement-breakpoint 5 | CREATE TYPE "public"."relationship_status" AS ENUM('friend', 'blocked', 'preferred', 'disliked', 'neutral');--> statement-breakpoint 6 | CREATE TYPE "public"."response_type" AS ENUM('tweet_create', 'tweet_reply', 'tweet_thread', 'discord_chat', 'discord_mod', 'discord_help', 'discord_welcome', 'telegram_chat', 'telegram_group', 'telegram_broadcast', 'slack_chat', 'slack_thread', 'slack_channel', 'slack_dm');--> statement-breakpoint 7 | CREATE TABLE IF NOT EXISTS "active_conversations" ( 8 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 9 | "character_id" uuid NOT NULL, 10 | "user_id" varchar(255) NOT NULL, 11 | "platform" varchar(50) NOT NULL, 12 | "chat_id" varchar(255) NOT NULL, 13 | "message_count" numeric DEFAULT '0' NOT NULL, 14 | "last_message_timestamp" timestamp DEFAULT now() NOT NULL, 15 | "metadata" jsonb DEFAULT '{}'::jsonb, 16 | "created_at" timestamp DEFAULT now() NOT NULL 17 | ); 18 | --> statement-breakpoint 19 | CREATE TABLE IF NOT EXISTS "conversation_history" ( 20 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 21 | "conversation_id" uuid NOT NULL, 22 | "user_id" varchar(255) NOT NULL, 23 | "character_id" uuid NOT NULL, 24 | "message" varchar(1000) NOT NULL, 25 | "sentiment" "sentiment", 26 | "metadata" jsonb DEFAULT '{"platform":"","messageType":""}'::jsonb, 27 | "created_at" timestamp DEFAULT now() NOT NULL 28 | ); 29 | --> statement-breakpoint 30 | CREATE TABLE IF NOT EXISTS "characters" ( 31 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 32 | "name" varchar(255) NOT NULL, 33 | "bio" text NOT NULL, 34 | "personality_traits" jsonb NOT NULL, 35 | "onchain" jsonb, 36 | "general_guidelines" jsonb DEFAULT '[]'::jsonb, 37 | "identity" jsonb, 38 | "response_styles" jsonb DEFAULT '{"default":{"tone":[],"personality":[],"guidelines":[]},"platforms":{}}'::jsonb NOT NULL, 39 | "styles" jsonb DEFAULT '{"chat":{"rules":[],"examples":[]},"professional":{"rules":[],"examples":[]},"casual":{"rules":[],"examples":[]}}'::jsonb, 40 | "should_respond" jsonb, 41 | "hobbies" jsonb DEFAULT '[]'::jsonb, 42 | "belief_system" jsonb DEFAULT '[]'::jsonb, 43 | "preferences" jsonb DEFAULT '{"preferredTopics":[],"dislikedTopics":[],"preferredTimes":[],"dislikedTimes":[],"preferredDays":[],"dislikedDays":[],"preferredHours":[],"dislikedHours":[],"generalLikes":[],"generalDislikes":[]}'::jsonb, 44 | "created_at" timestamp DEFAULT now() NOT NULL, 45 | "updated_at" timestamp DEFAULT now() NOT NULL 46 | ); 47 | --> statement-breakpoint 48 | CREATE TABLE IF NOT EXISTS "events" ( 49 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 50 | "character_id" uuid NOT NULL, 51 | "type" varchar(255) NOT NULL, 52 | "payload" jsonb NOT NULL, 53 | "metadata" jsonb NOT NULL, 54 | "processed" boolean DEFAULT false NOT NULL, 55 | "created_at" timestamp DEFAULT now() NOT NULL 56 | ); 57 | --> statement-breakpoint 58 | CREATE TABLE IF NOT EXISTS "goals" ( 59 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 60 | "character_id" uuid NOT NULL, 61 | "description" text NOT NULL, 62 | "status" varchar(50) DEFAULT 'active' NOT NULL, 63 | "progress" numeric DEFAULT '0' NOT NULL, 64 | "metadata" jsonb DEFAULT '{}'::jsonb, 65 | "created_at" timestamp DEFAULT now() NOT NULL, 66 | "completed_at" timestamp, 67 | "updated_at" timestamp DEFAULT now() NOT NULL 68 | ); 69 | --> statement-breakpoint 70 | CREATE TABLE IF NOT EXISTS "memories" ( 71 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 72 | "character_id" uuid NOT NULL, 73 | "type" "memory_type" NOT NULL, 74 | "content" text NOT NULL, 75 | "importance" numeric DEFAULT '0.5' NOT NULL, 76 | "metadata" jsonb DEFAULT '{}'::jsonb, 77 | "created_at" timestamp DEFAULT now() NOT NULL 78 | ); 79 | --> statement-breakpoint 80 | CREATE TABLE IF NOT EXISTS "social_relations" ( 81 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 82 | "character_id" uuid NOT NULL, 83 | "user_id" varchar(255) NOT NULL, 84 | "status" "relationship_status" DEFAULT 'neutral' NOT NULL, 85 | "interaction_count" numeric DEFAULT '0' NOT NULL, 86 | "sentiment" numeric DEFAULT '0' NOT NULL, 87 | "last_interaction" timestamp DEFAULT now() NOT NULL, 88 | "metadata" jsonb DEFAULT '{}'::jsonb, 89 | "created_at" timestamp DEFAULT now() NOT NULL, 90 | "updated_at" timestamp DEFAULT now() NOT NULL 91 | ); 92 | --> statement-breakpoint 93 | DO $$ BEGIN 94 | ALTER TABLE "conversation_history" ADD CONSTRAINT "conversation_history_conversation_id_active_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."active_conversations"("id") ON DELETE no action ON UPDATE no action; 95 | EXCEPTION 96 | WHEN duplicate_object THEN null; 97 | END $$; 98 | --> statement-breakpoint 99 | DO $$ BEGIN 100 | ALTER TABLE "events" ADD CONSTRAINT "events_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 101 | EXCEPTION 102 | WHEN duplicate_object THEN null; 103 | END $$; 104 | --> statement-breakpoint 105 | DO $$ BEGIN 106 | ALTER TABLE "goals" ADD CONSTRAINT "goals_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 107 | EXCEPTION 108 | WHEN duplicate_object THEN null; 109 | END $$; 110 | --> statement-breakpoint 111 | DO $$ BEGIN 112 | ALTER TABLE "memories" ADD CONSTRAINT "memories_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 113 | EXCEPTION 114 | WHEN duplicate_object THEN null; 115 | END $$; 116 | --> statement-breakpoint 117 | DO $$ BEGIN 118 | ALTER TABLE "social_relations" ADD CONSTRAINT "social_relations_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 119 | EXCEPTION 120 | WHEN duplicate_object THEN null; 121 | END $$; 122 | -------------------------------------------------------------------------------- /src/db/migrations/0005_crazy_raza.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."group_tier" AS ENUM('permanent', 'temporary');--> statement-breakpoint 2 | CREATE TYPE "public"."interaction_event_type" AS ENUM('interaction.started', 'interaction.completed', 'interaction.failed', 'interaction.rate_limited', 'interaction.invalid', 'interaction.cancelled', 'interaction.processed', 'interaction.queued');--> statement-breakpoint 3 | CREATE TABLE IF NOT EXISTS "telegram_groups" ( 4 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 5 | "telegram_id" varchar(255) NOT NULL, 6 | "tier" "group_tier" DEFAULT 'temporary' NOT NULL, 7 | "is_active" boolean DEFAULT true NOT NULL, 8 | "settings" jsonb DEFAULT '{"allowCommands":true,"adminUserIds":[]}'::jsonb, 9 | "metadata" jsonb NOT NULL, 10 | "created_at" timestamp DEFAULT now() NOT NULL, 11 | "updated_at" timestamp DEFAULT now() NOT NULL, 12 | CONSTRAINT "telegram_groups_telegram_id_unique" UNIQUE("telegram_id") 13 | ); 14 | --> statement-breakpoint 15 | DROP TABLE "active_conversations" CASCADE;--> statement-breakpoint 16 | DROP TABLE "conversation_history" CASCADE;--> statement-breakpoint 17 | ALTER TABLE "events" ADD COLUMN "interaction_event_type" "interaction_event_type" DEFAULT 'interaction.started' NOT NULL;--> statement-breakpoint 18 | ALTER TABLE "events" DROP COLUMN IF EXISTS "type";--> statement-breakpoint 19 | DROP TYPE "public"."sentiment"; -------------------------------------------------------------------------------- /src/db/migrations/0006_grey_speedball.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "social_relations" ALTER COLUMN "interaction_count" SET DATA TYPE integer;--> statement-breakpoint 2 | ALTER TABLE "social_relations" ALTER COLUMN "interaction_count" SET DEFAULT 0; -------------------------------------------------------------------------------- /src/db/migrations/0007_useful_darwin.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE "public"."interaction_event_type" ADD VALUE 'image.generation.started';--> statement-breakpoint 2 | ALTER TYPE "public"."interaction_event_type" ADD VALUE 'image.generation.completed';--> statement-breakpoint 3 | ALTER TYPE "public"."interaction_event_type" ADD VALUE 'image.generation.failed';--> statement-breakpoint 4 | ALTER TYPE "public"."interaction_event_type" ADD VALUE 'image.moderation.rejected';--> statement-breakpoint 5 | ALTER TYPE "public"."platform" ADD VALUE 'api'; -------------------------------------------------------------------------------- /src/db/migrations/0008_lean_machine_man.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "quantum_states" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "timestamp" timestamp DEFAULT now() NOT NULL, 4 | "random_value" integer NOT NULL, 5 | "mood_value" integer NOT NULL, 6 | "creativity_value" integer NOT NULL, 7 | "entropy_hash" text NOT NULL, 8 | "is_fallback" boolean DEFAULT false NOT NULL, 9 | "created_at" timestamp DEFAULT now() NOT NULL 10 | ); 11 | -------------------------------------------------------------------------------- /src/db/migrations/0009_lowly_the_watchers.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "characters" ADD COLUMN "quantum_personality" jsonb; -------------------------------------------------------------------------------- /src/db/migrations/0010_fine_boom_boom.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."twitter_mention_status" AS ENUM('pending', 'processed', 'skipped', 'failed', 'rate_limited');--> statement-breakpoint 2 | CREATE TABLE IF NOT EXISTS "twitter_mentions" ( 3 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 4 | "tweet_id" varchar(255) NOT NULL, 5 | "author_id" varchar(255) NOT NULL, 6 | "author_username" varchar(255) NOT NULL, 7 | "character_id" uuid NOT NULL, 8 | "created_at" timestamp NOT NULL, 9 | "processed_at" timestamp, 10 | "status" "twitter_mention_status" DEFAULT 'pending' NOT NULL, 11 | "skip_reason" varchar(255), 12 | "response_tweet_id" varchar(255), 13 | "is_reply" boolean DEFAULT false NOT NULL, 14 | "is_retweet" boolean DEFAULT false NOT NULL, 15 | "conversation_id" varchar(255), 16 | "metrics" jsonb, 17 | CONSTRAINT "twitter_mentions_tweet_id_unique" UNIQUE("tweet_id") 18 | ); 19 | --> statement-breakpoint 20 | ALTER TABLE "characters" ALTER COLUMN "quantum_personality" SET DEFAULT '{"temperature":0.7,"personalityTraits":[],"styleModifiers":{"tone":[],"guidelines":[]},"creativityLevels":{"low":{"personalityTraits":[],"styleModifiers":{"tone":[],"guidelines":[]}},"medium":{"personalityTraits":[],"styleModifiers":{"tone":[],"guidelines":[]}},"high":{"personalityTraits":[],"styleModifiers":{"tone":[],"guidelines":[]}}},"temperatureRange":{"min":0.6,"max":0.8},"creativityThresholds":{"low":100,"medium":180}}'::jsonb;--> statement-breakpoint 21 | ALTER TABLE "characters" ALTER COLUMN "quantum_personality" SET NOT NULL;--> statement-breakpoint 22 | DO $$ BEGIN 23 | ALTER TABLE "twitter_mentions" ADD CONSTRAINT "twitter_mentions_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 24 | EXCEPTION 25 | WHEN duplicate_object THEN null; 26 | END $$; 27 | -------------------------------------------------------------------------------- /src/db/migrations/0011_familiar_prism.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."coin_price_source" AS ENUM('coingecko', 'binance', 'kraken', 'manual');--> statement-breakpoint 2 | CREATE TABLE IF NOT EXISTS "coin_price_history" ( 3 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 4 | "coin_id" uuid NOT NULL, 5 | "price" numeric NOT NULL, 6 | "timestamp" timestamp NOT NULL, 7 | "source" "coin_price_source" DEFAULT 'coingecko' NOT NULL, 8 | "metadata" jsonb DEFAULT '{}'::jsonb, 9 | "created_at" timestamp DEFAULT now() NOT NULL 10 | ); 11 | --> statement-breakpoint 12 | CREATE TABLE IF NOT EXISTS "coins" ( 13 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 14 | "coingecko_id" varchar(255) NOT NULL, 15 | "symbol" varchar(50) NOT NULL, 16 | "name" varchar(255) NOT NULL, 17 | "current_price" numeric DEFAULT '0' NOT NULL, 18 | "price_change_24h" numeric DEFAULT '0' NOT NULL, 19 | "price_change_7d" numeric DEFAULT '0' NOT NULL, 20 | "platforms" jsonb DEFAULT '{}'::jsonb, 21 | "metadata" jsonb DEFAULT '{}'::jsonb, 22 | "twitterHandle" text, 23 | "last_checked" timestamp DEFAULT now() NOT NULL, 24 | "last_updated" timestamp DEFAULT now() NOT NULL, 25 | "created_at" timestamp DEFAULT now() NOT NULL, 26 | CONSTRAINT "coins_coingecko_id_unique" UNIQUE("coingecko_id") 27 | ); 28 | --> statement-breakpoint 29 | DO $$ BEGIN 30 | ALTER TABLE "coin_price_history" ADD CONSTRAINT "coin_price_history_coin_id_coins_id_fk" FOREIGN KEY ("coin_id") REFERENCES "public"."coins"("id") ON DELETE cascade ON UPDATE no action; 31 | EXCEPTION 32 | WHEN duplicate_object THEN null; 33 | END $$; 34 | -------------------------------------------------------------------------------- /src/db/migrations/0012_sticky_sway.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "coin_price_history" ADD COLUMN "rank" integer DEFAULT 0 NOT NULL; -------------------------------------------------------------------------------- /src/db/migrations/0013_furry_squadron_supreme.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "coins" ADD COLUMN "rank" integer DEFAULT 0 NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "coin_price_history" DROP COLUMN IF EXISTS "rank"; -------------------------------------------------------------------------------- /src/db/migrations/0014_fantastic_squadron_supreme.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "goals" ADD COLUMN "type" varchar(50) NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "goals" ADD COLUMN "priority" integer DEFAULT 0 NOT NULL; -------------------------------------------------------------------------------- /src/db/migrations/0015_young_gateway.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS "goals" CASCADE; -------------------------------------------------------------------------------- /src/db/migrations/0016_wakeful_mastermind.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."goal_priority" AS ENUM('low', 'medium', 'high', 'critical');--> statement-breakpoint 2 | CREATE TYPE "public"."goal_status" AS ENUM('pending', 'active', 'paused', 'completed', 'failed');--> statement-breakpoint 3 | CREATE TYPE "public"."goal_type" AS ENUM('maintenance', 'learning', 'interaction', 'creation', 'analysis', 'social', 'market');--> statement-breakpoint 4 | CREATE TABLE IF NOT EXISTS "goal_dependencies" ( 5 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 6 | "goal_id" uuid NOT NULL, 7 | "depends_on_id" uuid NOT NULL, 8 | "type" text NOT NULL, 9 | "metadata" jsonb, 10 | "created_at" timestamp DEFAULT now() NOT NULL 11 | ); 12 | --> statement-breakpoint 13 | CREATE TABLE IF NOT EXISTS "goal_executions" ( 14 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 15 | "goal_id" uuid NOT NULL, 16 | "started_at" timestamp NOT NULL, 17 | "completed_at" timestamp, 18 | "success" boolean, 19 | "progress" integer DEFAULT 0 NOT NULL, 20 | "quantum_state" jsonb, 21 | "result" jsonb, 22 | "metadata" jsonb 23 | ); 24 | --> statement-breakpoint 25 | CREATE TABLE IF NOT EXISTS "goals" ( 26 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 27 | "character_id" uuid NOT NULL, 28 | "type" "goal_type" NOT NULL, 29 | "priority" "goal_priority" DEFAULT 'medium' NOT NULL, 30 | "status" "goal_status" DEFAULT 'pending' NOT NULL, 31 | "description" text NOT NULL, 32 | "progress" integer DEFAULT 0 NOT NULL, 33 | "is_recurring" boolean DEFAULT false, 34 | "criteria" jsonb NOT NULL, 35 | "quantum_preferences" jsonb, 36 | "execution_settings" jsonb, 37 | "metadata" jsonb, 38 | "created_at" timestamp DEFAULT now() NOT NULL, 39 | "updated_at" timestamp DEFAULT now() NOT NULL, 40 | "completed_at" timestamp, 41 | "next_scheduled_at" timestamp 42 | ); 43 | --> statement-breakpoint 44 | DO $$ BEGIN 45 | ALTER TABLE "goal_dependencies" ADD CONSTRAINT "goal_dependencies_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE cascade ON UPDATE no action; 46 | EXCEPTION 47 | WHEN duplicate_object THEN null; 48 | END $$; 49 | --> statement-breakpoint 50 | DO $$ BEGIN 51 | ALTER TABLE "goal_dependencies" ADD CONSTRAINT "goal_dependencies_depends_on_id_goals_id_fk" FOREIGN KEY ("depends_on_id") REFERENCES "public"."goals"("id") ON DELETE cascade ON UPDATE no action; 52 | EXCEPTION 53 | WHEN duplicate_object THEN null; 54 | END $$; 55 | --> statement-breakpoint 56 | DO $$ BEGIN 57 | ALTER TABLE "goal_executions" ADD CONSTRAINT "goal_executions_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE cascade ON UPDATE no action; 58 | EXCEPTION 59 | WHEN duplicate_object THEN null; 60 | END $$; 61 | --> statement-breakpoint 62 | DO $$ BEGIN 63 | ALTER TABLE "goals" ADD CONSTRAINT "goals_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; 64 | EXCEPTION 65 | WHEN duplicate_object THEN null; 66 | END $$; 67 | -------------------------------------------------------------------------------- /src/db/migrations/0017_ancient_rocket_raccoon.sql: -------------------------------------------------------------------------------- 1 | ALTER TYPE "public"."platform" ADD VALUE 'system'; -------------------------------------------------------------------------------- /src/db/migrations/0018_workable_praxagora.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE "goal_dependencies" CASCADE;--> statement-breakpoint 2 | DROP TABLE "goal_executions" CASCADE;--> statement-breakpoint 3 | ALTER TABLE "goals" ADD COLUMN "name" text NOT NULL;--> statement-breakpoint 4 | ALTER TABLE "goals" ADD COLUMN "daily_frequency" integer DEFAULT 1 NOT NULL;--> statement-breakpoint 5 | ALTER TABLE "goals" ADD COLUMN "hourly_frequency" integer DEFAULT 1 NOT NULL;--> statement-breakpoint 6 | ALTER TABLE "goals" ADD COLUMN "tools" text[];--> statement-breakpoint 7 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "type";--> statement-breakpoint 8 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "priority";--> statement-breakpoint 9 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "status";--> statement-breakpoint 10 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "progress";--> statement-breakpoint 11 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "is_recurring";--> statement-breakpoint 12 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "criteria";--> statement-breakpoint 13 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "quantum_preferences";--> statement-breakpoint 14 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "execution_settings";--> statement-breakpoint 15 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "completed_at";--> statement-breakpoint 16 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "next_scheduled_at";--> statement-breakpoint 17 | DROP TYPE "public"."goal_priority";--> statement-breakpoint 18 | DROP TYPE "public"."goal_status";--> statement-breakpoint 19 | DROP TYPE "public"."goal_type"; -------------------------------------------------------------------------------- /src/db/migrations/0019_good_patch.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "goals" DROP CONSTRAINT "goals_character_id_characters_id_fk"; 2 | --> statement-breakpoint 3 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "character_id"; -------------------------------------------------------------------------------- /src/db/migrations/0020_public_guardsmen.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "goal_tracker" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "goal_id" uuid NOT NULL, 4 | "last_ran_at" timestamp, 5 | "metadata" jsonb, 6 | "created_at" timestamp DEFAULT now() NOT NULL, 7 | "updated_at" timestamp DEFAULT now() NOT NULL 8 | ); 9 | --> statement-breakpoint 10 | DO $$ BEGIN 11 | ALTER TABLE "goal_tracker" ADD CONSTRAINT "goal_tracker_goal_id_goals_id_fk" FOREIGN KEY ("goal_id") REFERENCES "public"."goals"("id") ON DELETE cascade ON UPDATE no action; 12 | EXCEPTION 13 | WHEN duplicate_object THEN null; 14 | END $$; 15 | -------------------------------------------------------------------------------- /src/db/migrations/0021_early_spiral.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "goal_tracker" ADD COLUMN "total_runs_daily" integer DEFAULT 0 NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "goal_tracker" ADD COLUMN "total_runs_hourly" integer DEFAULT 0 NOT NULL; -------------------------------------------------------------------------------- /src/db/migrations/0022_lumpy_boomer.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE "public"."goal_type" AS ENUM('post');--> statement-breakpoint 2 | ALTER TABLE "goals" ADD COLUMN "type" "goal_type" DEFAULT 'post' NOT NULL; -------------------------------------------------------------------------------- /src/db/migrations/0023_bouncy_sally_floyd.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "goal_tracker" ADD COLUMN "last_daily_reset_at" timestamp;--> statement-breakpoint 2 | ALTER TABLE "goal_tracker" ADD COLUMN "last_hourly_reset_at" timestamp; -------------------------------------------------------------------------------- /src/db/migrations/0024_watery_mandrill.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "goals" ADD COLUMN "context_types" text[];--> statement-breakpoint 2 | ALTER TABLE "goals" DROP COLUMN IF EXISTS "metadata"; -------------------------------------------------------------------------------- /src/db/migrations/meta/0003_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4c925486-f91e-491d-a74c-3df377d8238d", 3 | "prevId": "e09c533f-e556-4879-8940-d63b503239aa", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": {}, 7 | "enums": {}, 8 | "schemas": {}, 9 | "sequences": {}, 10 | "roles": {}, 11 | "policies": {}, 12 | "views": {}, 13 | "_meta": { 14 | "columns": {}, 15 | "schemas": {}, 16 | "tables": {} 17 | } 18 | } -------------------------------------------------------------------------------- /src/db/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1731946154347, 9 | "tag": "0000_orange_living_tribunal", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1732120405061, 16 | "tag": "0001_small_cerise", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "7", 22 | "when": 1732135759182, 23 | "tag": "0002_sturdy_spirit", 24 | "breakpoints": true 25 | }, 26 | { 27 | "idx": 3, 28 | "version": "7", 29 | "when": 1732145772202, 30 | "tag": "0003_mature_phil_sheldon", 31 | "breakpoints": true 32 | }, 33 | { 34 | "idx": 4, 35 | "version": "7", 36 | "when": 1732147355768, 37 | "tag": "0004_numerous_storm", 38 | "breakpoints": true 39 | }, 40 | { 41 | "idx": 5, 42 | "version": "7", 43 | "when": 1732202492404, 44 | "tag": "0005_crazy_raza", 45 | "breakpoints": true 46 | }, 47 | { 48 | "idx": 6, 49 | "version": "7", 50 | "when": 1732220038567, 51 | "tag": "0006_grey_speedball", 52 | "breakpoints": true 53 | }, 54 | { 55 | "idx": 7, 56 | "version": "7", 57 | "when": 1732309520757, 58 | "tag": "0007_useful_darwin", 59 | "breakpoints": true 60 | }, 61 | { 62 | "idx": 8, 63 | "version": "7", 64 | "when": 1732624784877, 65 | "tag": "0008_lean_machine_man", 66 | "breakpoints": true 67 | }, 68 | { 69 | "idx": 9, 70 | "version": "7", 71 | "when": 1732638464629, 72 | "tag": "0009_lowly_the_watchers", 73 | "breakpoints": true 74 | }, 75 | { 76 | "idx": 10, 77 | "version": "7", 78 | "when": 1732711309729, 79 | "tag": "0010_fine_boom_boom", 80 | "breakpoints": true 81 | }, 82 | { 83 | "idx": 11, 84 | "version": "7", 85 | "when": 1732744870531, 86 | "tag": "0011_familiar_prism", 87 | "breakpoints": true 88 | }, 89 | { 90 | "idx": 12, 91 | "version": "7", 92 | "when": 1732805237593, 93 | "tag": "0012_sticky_sway", 94 | "breakpoints": true 95 | }, 96 | { 97 | "idx": 13, 98 | "version": "7", 99 | "when": 1732805261530, 100 | "tag": "0013_furry_squadron_supreme", 101 | "breakpoints": true 102 | }, 103 | { 104 | "idx": 14, 105 | "version": "7", 106 | "when": 1733133226219, 107 | "tag": "0014_fantastic_squadron_supreme", 108 | "breakpoints": true 109 | }, 110 | { 111 | "idx": 15, 112 | "version": "7", 113 | "when": 1733134204082, 114 | "tag": "0015_young_gateway", 115 | "breakpoints": true 116 | }, 117 | { 118 | "idx": 16, 119 | "version": "7", 120 | "when": 1733134280936, 121 | "tag": "0016_wakeful_mastermind", 122 | "breakpoints": true 123 | }, 124 | { 125 | "idx": 17, 126 | "version": "7", 127 | "when": 1733134458239, 128 | "tag": "0017_ancient_rocket_raccoon", 129 | "breakpoints": true 130 | }, 131 | { 132 | "idx": 18, 133 | "version": "7", 134 | "when": 1733402245145, 135 | "tag": "0018_workable_praxagora", 136 | "breakpoints": true 137 | }, 138 | { 139 | "idx": 19, 140 | "version": "7", 141 | "when": 1733403537923, 142 | "tag": "0019_good_patch", 143 | "breakpoints": true 144 | }, 145 | { 146 | "idx": 20, 147 | "version": "7", 148 | "when": 1733403599560, 149 | "tag": "0020_public_guardsmen", 150 | "breakpoints": true 151 | }, 152 | { 153 | "idx": 21, 154 | "version": "7", 155 | "when": 1733403620612, 156 | "tag": "0021_early_spiral", 157 | "breakpoints": true 158 | }, 159 | { 160 | "idx": 22, 161 | "version": "7", 162 | "when": 1733404955163, 163 | "tag": "0022_lumpy_boomer", 164 | "breakpoints": true 165 | }, 166 | { 167 | "idx": 23, 168 | "version": "7", 169 | "when": 1733406555316, 170 | "tag": "0023_bouncy_sally_floyd", 171 | "breakpoints": true 172 | }, 173 | { 174 | "idx": 24, 175 | "version": "7", 176 | "when": 1733407150184, 177 | "tag": "0024_watery_mandrill", 178 | "breakpoints": true 179 | } 180 | ] 181 | } -------------------------------------------------------------------------------- /src/db/schema/goal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | integer, 3 | jsonb, 4 | pgEnum, 5 | pgTable, 6 | text, 7 | timestamp, 8 | uuid, 9 | } from "drizzle-orm/pg-core"; 10 | 11 | export const GoalType = pgEnum("goal_type", ["post"]); 12 | 13 | export const goals = pgTable("goals", { 14 | id: uuid("id").defaultRandom().primaryKey(), 15 | name: text("name").notNull(), 16 | description: text("description").notNull(), 17 | dailyFrequency: integer("daily_frequency").notNull().default(1), 18 | hourlyFrequency: integer("hourly_frequency").notNull().default(1), 19 | type: GoalType("type").notNull().default("post"), 20 | tools: text("tools").array(), 21 | contextTypes: text("context_types").array(), 22 | createdAt: timestamp("created_at").notNull().defaultNow(), 23 | updatedAt: timestamp("updated_at").notNull().defaultNow(), 24 | }); 25 | 26 | export const goalTracker = pgTable("goal_tracker", { 27 | id: uuid("id").defaultRandom().primaryKey(), 28 | goalId: uuid("goal_id") 29 | .notNull() 30 | .references(() => goals.id, { onDelete: "cascade" }), 31 | lastRanAt: timestamp("last_ran_at"), 32 | // New fields for UTC tracking 33 | lastDailyResetAt: timestamp("last_daily_reset_at"), 34 | lastHourlyResetAt: timestamp("last_hourly_reset_at"), 35 | totalRunsDaily: integer("total_runs_daily").notNull().default(0), 36 | totalRunsHourly: integer("total_runs_hourly").notNull().default(0), 37 | metadata: jsonb("metadata"), 38 | createdAt: timestamp("created_at").notNull().defaultNow(), 39 | updatedAt: timestamp("updated_at").notNull().defaultNow(), 40 | }); 41 | 42 | export type Goal = typeof goals.$inferSelect; 43 | export type NewGoal = typeof goals.$inferInsert; 44 | -------------------------------------------------------------------------------- /src/docs/QUANTUM_ELI5.md: -------------------------------------------------------------------------------- 1 | # Quantum Entropy: What We Know (and Don't Know) 2 | 3 | ## The Circuit Basics 4 | 5 | Our quantum circuit actually does: 6 | 7 | ``` 8 | First, each qubit gets hit with an sx gate (square root of NOT) - this creates a partial superposition 9 | Then it applies rz (rotation around Z-axis) gates with different angles: 10 | 11 | First 8 qubits: π/2 rotation (stronger) 12 | Last 8 qubits: π/8 rotation (more subtle) 13 | 14 | 15 | Finally another sx gate before measurement 16 | ``` 17 | 18 | ## What We Actually Know 19 | 20 | 1. **Measurement Results**: 21 | 22 | - We get 1024 measurements of 16 qubits 23 | - Each measurement gives us a 16-bit string 24 | - The distribution of these results determines entropy 25 | 26 | 2. **Sources of Variation**: 27 | - Quantum superposition and interference 28 | - Hardware noise and imperfections 29 | - Environmental effects on the quantum system 30 | - Decoherence during operation 31 | 32 | ## What Makes This Quantum (Not Just Random) 33 | 34 | The entropy variations come from several quantum effects: 35 | 36 | 1. Quantum interference between the SX gates and RZ rotations 37 | 2. The way superposition states evolve 38 | 3. How quantum measurements collapse these states 39 | 4. Real quantum hardware effects 40 | 41 | ## Why We See Different Entropy Levels 42 | 43 | We can actually partially explain the variations: 44 | 45 | - High entropy runs: Superposition states remained more "quantum" 46 | - Low entropy runs: Some quantum states may have decohered more 47 | - Medium entropy: Mix of these effects 48 | 49 | However, we cannot predict exactly what will happen in any given run - this is fundamental quantum behavior, not just randomness or noise. 50 | 51 | ## The Real Mystery 52 | 53 | What we truly don't know: 54 | 55 | - Exact state of the quantum system between operations 56 | - Precise impact of environmental interactions 57 | - Why specific patterns emerge in specific runs 58 | 59 | This is different from classical randomness because: 60 | 61 | 1. The variations have quantum signatures 62 | 2. The patterns show quantum interference effects 63 | 3. The results depend on quantum mechanical principles 64 | 65 | ## Important Note 66 | 67 | While we don't know the exact outcome of each run, we do understand the quantum mechanics behind why variations occur. The mystery isn't in why there are variations, but in predicting specific outcomes. 68 | -------------------------------------------------------------------------------- /src/docs/flow.MD: -------------------------------------------------------------------------------- 1 | # Flows 2 | 3 | ```mermaid 4 | flowchart TB 5 | subgraph Client["Client Layer"] 6 | TG["Telegram Client"] 7 | TW["Twitter Client"] 8 | end 9 | 10 | subgraph Handler["Handler Layer"] 11 | MH["Message Handler"] 12 | PS["Prompt Service"] 13 | end 14 | 15 | subgraph Core["AI Core"] 16 | AIM["AI Manager (Singleton)"] 17 | AI["AI Class"] 18 | end 19 | 20 | User --> TG 21 | User --> TW 22 | 23 | TG --> |"handle(ctx)"| MH 24 | TW -.-> |"future implementation"| MH 25 | 26 | MH --> |"builds context"| PS 27 | MH --> |"gets defaults"| AIM 28 | 29 | PS --> |"formatHistory\nbuildSystemPrompt"| MH 30 | AIM --> |"getInteractionDefaults\ncompleteOptions"| MH 31 | 32 | MH --> |"interact()"| AI 33 | 34 | subgraph Services["Core Services"] 35 | IS["Interaction Service"] 36 | ES["Event Service"] 37 | CM["Conversation Manager"] 38 | end 39 | 40 | AI --> |"processes interaction"| CM 41 | CM --> |"handles interaction"| IS 42 | IS --> |"logs events"| ES 43 | ``` 44 | 45 | # AI Class 46 | 47 | ```mermaid 48 | flowchart TB 49 | subgraph Input["Input Processing"] 50 | MSG["Message/Input"] 51 | OPT["Options"] 52 | end 53 | 54 | subgraph ConversationManager["Conversation Manager"] 55 | CV["Validate Group"] 56 | CS["Update Conv. State"] 57 | SR["Check Should Respond"] 58 | CC["Build Context"] 59 | end 60 | 61 | subgraph InteractionService["Interaction Service"] 62 | CH["Character Handler"] 63 | ST["Style Manager"] 64 | LL["LLM Manager"] 65 | MM["Memory Manager"] 66 | TM["Tool Manager"] 67 | end 68 | 69 | subgraph EventService["Event Service"] 70 | EV1["interaction.started"] 71 | EV2["interaction.completed"] 72 | EV3["interaction.failed"] 73 | end 74 | 75 | MSG & OPT --> |"handleMessage()"| ConversationManager 76 | 77 | CV --> CS 78 | CS --> SR 79 | SR --> |"shouldRespond=true"| CC 80 | 81 | CC --> |"context"| InteractionService 82 | 83 | CH --> |"get character"| ST 84 | ST --> |"apply style"| LL 85 | LL --> |"generate response"| MM 86 | MM --> |"update memory"| TM 87 | 88 | InteractionService --> |"success"| EV2 89 | InteractionService --> |"failure"| EV3 90 | 91 | subgraph Database["Database Layer"] 92 | direction TB 93 | T1["characters"] 94 | T2["events"] 95 | T3["memories"] 96 | T4["social_relations"] 97 | T5["telegram_groups"] 98 | end 99 | 100 | ConversationManager --> |"read/write"| Database 101 | InteractionService --> |"read/write"| Database 102 | EventService --> |"write"| Database 103 | ``` 104 | 105 | # Manager Flow 106 | 107 | ```mermaid 108 | sequenceDiagram 109 | participant U as User 110 | participant TC as TelegramClient 111 | participant MH as MessageHandler 112 | participant PS as PromptService 113 | participant AIM as AIManager 114 | participant CM as ConversationManager 115 | participant IS as InteractionService 116 | participant DB as Database 117 | participant LLM as LLMManager 118 | 119 | U->>TC: sends message 120 | TC->>MH: handle(ctx) 121 | 122 | MH->>MH: validateMessage() 123 | MH->>MH: checkRateLimit() 124 | MH->>MH: setProcessingState() 125 | 126 | MH->>PS: addToHistory() 127 | MH->>PS: formatChatHistory() 128 | MH->>PS: buildSystemPrompt() 129 | 130 | MH->>AIM: getInteractionDefaults() 131 | MH->>AIM: completeInteractionOptions() 132 | 133 | MH->>CM: handleMessage() 134 | 135 | CM->>DB: validateGroup() 136 | CM->>DB: updateConversationState() 137 | CM->>LLM: analyzeImportance() 138 | CM->>CM: shouldRespond() 139 | 140 | CM->>IS: handleInteraction() 141 | 142 | IS->>DB: getCharacter() 143 | IS->>DB: getMemories() 144 | IS->>LLM: generateResponse() 145 | IS->>DB: updateMemories() 146 | 147 | IS-->>CM: response 148 | CM-->>MH: response 149 | 150 | MH->>PS: addToHistory() 151 | MH->>TC: sendResponse() 152 | TC->>U: displays response 153 | ``` 154 | -------------------------------------------------------------------------------- /src/types/character.ts: -------------------------------------------------------------------------------- 1 | import type { QuantumPersonalityConfig } from "@/core/types"; 2 | import type { ResponseStyles } from "./style"; 3 | 4 | export type CharacterTrait = { 5 | name: string; 6 | value: number; 7 | description?: string; 8 | }; 9 | 10 | export type Hobby = { 11 | name: string; 12 | proficiency?: number; 13 | interest?: number; 14 | lastPracticed?: string; 15 | relatedTopics?: string[]; 16 | metadata?: Record; 17 | }; 18 | 19 | export type Preferences = { 20 | preferredTopics: string[]; 21 | dislikedTopics: string[]; 22 | preferredTimes?: string[]; 23 | dislikedTimes?: string[]; 24 | preferredDays?: string[]; 25 | dislikedDays?: string[]; 26 | preferredHours?: string[]; 27 | dislikedHours?: string[]; 28 | generalLikes: string[]; 29 | generalDislikes: string[]; 30 | }; 31 | 32 | export type CreateCharacterInput = { 33 | name: string; 34 | bio: string; 35 | onchain?: { 36 | [key: string]: string; 37 | }; 38 | personalityTraits: string[]; 39 | hobbies?: Hobby[]; 40 | preferences?: Preferences; 41 | responseStyles?: ResponseStyles; 42 | identity?: { 43 | [key: string]: string | string[]; 44 | }; 45 | styles?: { 46 | [key: string]: { 47 | rules: string[]; 48 | examples: string[]; 49 | }; 50 | }; 51 | beliefSystem?: string[]; 52 | shouldRespond?: { 53 | rules: string[]; 54 | examples: string[]; 55 | }; 56 | goals?: Array<{ 57 | description: string; 58 | status?: "active" | "completed" | "paused"; 59 | progress?: number; 60 | metadata?: { 61 | dependencies?: string[]; 62 | completionCriteria?: string[]; 63 | notes?: string[]; 64 | }; 65 | }>; 66 | quantumPersonality?: QuantumPersonalityConfig; 67 | }; 68 | 69 | export type CharacterUpdate = Partial & { 70 | updatedAt?: Date; 71 | responseStyles?: ResponseStyles; 72 | }; 73 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./character"; 2 | export * from "./llm"; 3 | export * from "./memory"; 4 | export * from "./style"; 5 | 6 | // Add any shared types or unions here 7 | export type Timestamp = string | Date; 8 | 9 | export type BaseMetadata = { 10 | timestamp: Timestamp; 11 | source: string; 12 | version?: string; 13 | [key: string]: any; 14 | }; 15 | 16 | export type ErrorResponse = { 17 | error: string; 18 | code: string; 19 | details?: Record; 20 | }; 21 | 22 | export type SuccessResponse = { 23 | data: T; 24 | metadata?: BaseMetadata; 25 | }; 26 | 27 | export type APIResponse = SuccessResponse | ErrorResponse; 28 | 29 | // Add utility type helpers 30 | export type DeepPartial = { 31 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; 32 | }; 33 | 34 | export type RequireAtLeastOne = Pick< 35 | T, 36 | Exclude 37 | > & 38 | { 39 | [K in Keys]-?: Required> & Partial>>; 40 | }[Keys]; 41 | -------------------------------------------------------------------------------- /src/types/llm.ts: -------------------------------------------------------------------------------- 1 | export interface LLMConfig { 2 | apiKey: string; 3 | baseURL?: string; 4 | llm: { 5 | model: string; 6 | temperature?: number; 7 | topP?: number; 8 | frequencyPenalty?: number; 9 | presencePenalty?: number; 10 | stop?: string[]; 11 | [key: string]: any; 12 | }; 13 | analyzer: { 14 | model: string; 15 | temperature?: number; 16 | [key: string]: any; 17 | }; 18 | } 19 | 20 | export interface LLMResponse { 21 | content: string; 22 | metadata: { 23 | model: string; 24 | usage?: { 25 | promptTokens: number; 26 | completionTokens: number; 27 | totalTokens: number; 28 | }; 29 | finishReason?: string; 30 | [key: string]: any; 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/types/memory.ts: -------------------------------------------------------------------------------- 1 | export type MemoryType = "interaction" | "learning" | "achievement" | "hobby"; 2 | 3 | export type Memory = { 4 | id: string; 5 | characterId: string; 6 | type: MemoryType; 7 | content: string; 8 | importance: string; 9 | metadata?: { 10 | userId?: string; 11 | sentiment?: string; 12 | topic?: string; 13 | hobby?: string; 14 | relatedMemories?: string[]; 15 | [key: string]: any; 16 | }; 17 | createdAt: Date; 18 | }; 19 | 20 | export type CreateMemoryInput = Omit; 21 | -------------------------------------------------------------------------------- /src/types/style.ts: -------------------------------------------------------------------------------- 1 | import { dbSchemas } from "@/db"; 2 | 3 | export type Platform = (typeof dbSchemas.platformEnum.enumValues)[number]; 4 | 5 | export interface CustomInjection { 6 | name: string; 7 | content: string; 8 | position: "before" | "after" | "replace"; 9 | } 10 | 11 | export interface InteractionDefaults { 12 | mode: "enhanced"; 13 | platform: Platform; 14 | responseType: string; 15 | tools: string[]; 16 | injections: { 17 | injectPersonality: boolean; 18 | injectStyle: true; 19 | customInjections: CustomInjection[]; 20 | }; 21 | } 22 | 23 | export type CustomResponseType = `custom_${string}`; 24 | export type ResponseType = 25 | | (typeof dbSchemas.responseTypeEnum.enumValues)[number] 26 | | CustomResponseType; 27 | 28 | export interface StyleSettings { 29 | enabled?: boolean; 30 | tone?: string[]; 31 | guidelines: string[]; 32 | platform?: Platform; 33 | formatting: { 34 | maxLength?: number; 35 | allowMarkdown?: boolean; 36 | customRules?: string[]; 37 | }; 38 | rules?: string[]; 39 | } 40 | 41 | export interface PlatformStylesInput { 42 | enabled: boolean; 43 | defaultTone?: string[]; 44 | defaultGuidelines: string[]; 45 | styles: { 46 | [key: string]: StyleSettings; 47 | }; 48 | } 49 | 50 | export interface PlatformStyles { 51 | enabled: boolean; 52 | defaultTone?: string[]; 53 | defaultGuidelines: string[]; 54 | styles: { 55 | [K in ResponseType]?: StyleSettings; 56 | }; 57 | } 58 | 59 | export interface ResponseStyles { 60 | default: { 61 | tone?: string[]; 62 | personality?: string[]; 63 | guidelines: string[]; 64 | }; 65 | platforms: { 66 | [P in Platform]?: PlatformStyles; 67 | }; 68 | customTypes?: { 69 | [K in CustomResponseType]?: { 70 | platform: Platform; 71 | description?: string; 72 | }; 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/types/tools.ts: -------------------------------------------------------------------------------- 1 | export interface ToolResult { 2 | success: boolean; 3 | data: any; 4 | error?: string; 5 | } 6 | 7 | export interface Tool { 8 | name: string; 9 | execute: (params?: any) => Promise; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "types": ["node"], 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "target": "ESNext", 7 | "module": "ESNext", 8 | "moduleDetection": "force", 9 | "jsx": "react-jsx", 10 | "allowJs": true, 11 | 12 | // Bundler mode 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "verbatimModuleSyntax": true, 16 | "noEmit": true, 17 | 18 | // Best practices 19 | "strict": true, 20 | "skipLibCheck": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false, 27 | "baseUrl": "./", 28 | "paths": { 29 | "@/*": ["src/*"] 30 | } 31 | }, 32 | "exclude": ["src/example/ai/tools"] 33 | } 34 | --------------------------------------------------------------------------------