From c85ab94f70fffb4e42e692616dbe6ede66c70833 Mon Sep 17 00:00:00 2001 From: damengrandom Date: Sun, 26 Jul 2026 15:51:00 +1000 Subject: [PATCH 1/2] feat(T22): replace Anthropic SDK with OpenAI-compatible provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap every agent off @langchain/anthropic onto any OpenAI-compatible endpoint, defaulting to OpenRouter with the free nemotron model. - add src/agent/llm.ts — single createLlm(temperature) factory, provider selected by LLM_BASE_URL (OpenRouter default, NVIDIA NIM supported) - drop the six duplicated ChatAnthropic instances across the agents - remove @langchain/anthropic; bump @langchain/core for the openai peer - swap ANTHROPIC_API_KEY for LLM_API_KEY in the three workflows - disable the mock user seeding cron (workflow_dispatch retained) Trending digest improvements: - align repo count at 5-8 across prompt, tool schema and user message; the schema description was silently overriding the prompt's "1-3" - bound summaries to 140 chars in code, not just in the prompt - restrict tags to a closed vocabulary so hashtags stay searchable - escape url and tags before rendering into the HTML message Fix total star counts always rendering as 0: GitHub puts an svg icon between the stargazers anchor and the number, so the regex never matched. Verified against the live page (0 matches before, 18 after). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CFr2cPw9xvDvStaya9sDW8 --- .env.example | 8 +- .github/workflows/cleanup.yml | 2 +- .github/workflows/daily-kpi.yml | 2 +- .github/workflows/morning-news.yml | 2 +- .github/workflows/seed-mock-users.yml | 12 +- README.md | 2 +- package.json | 4 +- pnpm-lock.yaml | 158 +++++++++++--------------- src/agent/cleanup.agent.ts | 7 +- src/agent/diary.agent.ts | 7 +- src/agent/github.agent.ts | 7 +- src/agent/llm.ts | 17 +++ src/agent/manual-kpi.agent.ts | 7 +- src/agent/news-curator.agent.ts | 7 +- src/agent/news-telegram.agent.ts | 7 +- src/agent/prompt.ts | 9 +- src/constants/index.ts | 3 +- src/tools/news-curator.tool.ts | 8 +- src/tools/news-telegram.tool.ts | 9 +- src/tools/trending-scrape.tool.ts | 5 +- 20 files changed, 138 insertions(+), 145 deletions(-) create mode 100644 src/agent/llm.ts diff --git a/.env.example b/.env.example index 95a7506..c990313 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,9 @@ -# Anthropic -ANTHROPIC_API_KEY=your_anthropic_api_key_here +# LLM — any OpenAI-compatible provider. Only the key is required; +# it defaults to OpenRouter + nvidia/nemotron-3-ultra-550b-a55b:free +LLM_API_KEY=sk-or-v1-your_openrouter_key_here +# Optional overrides, e.g. to use NVIDIA NIM instead: +# LLM_BASE_URL=https://integrate.api.nvidia.com/v1 +# LLM_MODEL=nvidia/nemotron-3-ultra-550b-a55b # GitHub GITHUB_TOKEN=your_github_personal_access_token_here TARGET_GITHUB_USERNAME=your_github_username_here diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 3b65a4d..0656b11 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -31,7 +31,7 @@ jobs: - name: Run cleanup env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} DATABASE_URL: ${{ secrets.NEON_WORK_COORDINATOR_DB_URL }} COMPANY_DB_URL: ${{ secrets.NEON_MOCK_COMPANY_DB_URL }} COMPANY_CLEANUP_TABLE: ${{ secrets.COMPANY_CLEANUP_TABLE }} diff --git a/.github/workflows/daily-kpi.yml b/.github/workflows/daily-kpi.yml index 14cc4ca..10df088 100644 --- a/.github/workflows/daily-kpi.yml +++ b/.github/workflows/daily-kpi.yml @@ -31,7 +31,7 @@ jobs: - name: Run daily KPI report env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} DATABASE_URL: ${{ secrets.NEON_WORK_COORDINATOR_DB_URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TARGET_GITHUB_USERNAME: ${{ secrets.TARGET_GITHUB_USERNAME }} diff --git a/.github/workflows/morning-news.yml b/.github/workflows/morning-news.yml index 89d0514..ab5bf39 100644 --- a/.github/workflows/morning-news.yml +++ b/.github/workflows/morning-news.yml @@ -30,7 +30,7 @@ jobs: - name: Run news agent env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} DATABASE_URL: ${{ secrets.NEON_WORK_COORDINATOR_DB_URL }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} diff --git a/.github/workflows/seed-mock-users.yml b/.github/workflows/seed-mock-users.yml index e18ac30..ed8498c 100644 --- a/.github/workflows/seed-mock-users.yml +++ b/.github/workflows/seed-mock-users.yml @@ -1,11 +1,13 @@ name: Daily Mock User Seeding on: - schedule: - # 6:30 AM UTC = 4:30 PM AEST (UTC+10, Apr–Oct) - # Note: during AEDT (Oct–Apr, UTC+11) this fires at 5:30 PM Sydney time - - cron: '30 6 * * *' - workflow_dispatch: # allows manual trigger from GitHub UI + # Disabled — mock user seeding no longer runs on a schedule. + # Uncomment to re-enable; the job itself is unchanged. + # schedule: + # # 6:30 AM UTC = 4:30 PM AEST (UTC+10, Apr–Oct) + # # Note: during AEDT (Oct–Apr, UTC+11) this fires at 5:30 PM Sydney time + # - cron: '30 6 * * *' + workflow_dispatch: # still runnable by hand from the GitHub UI jobs: seed: diff --git a/README.md b/README.md index 555ef19..90c0789 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Push to GitHub, then add these secrets under **Settings → Secrets and variable | Secret | Value | |---|---| -| `ANTHROPIC_API_KEY` | from console.anthropic.com | +| `LLM_API_KEY` | from openrouter.ai/keys (free tier works) | | `NEON_WORK_COORDINATOR_DB_URL` | Neon connection string (remove `&channel_binding=require`) | | `NEON_MOCK_COMPANY_DB_URL` | Neon connection string for company DB | | `COMPANY_CLEANUP_TABLE` | table to clean, e.g. `mockTestUsers` | diff --git a/package.json b/package.json index 0a42408..e2ad69e 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "license": "ISC", "packageManager": "pnpm@10.7.1", "dependencies": { - "@langchain/anthropic": "^1.3.25", - "@langchain/core": "^1.1.36", + "@langchain/core": "^1.2.3", + "@langchain/openai": "^1.5.5", "@octokit/rest": "^22.0.1", "dotenv": "^17.3.1", "langchain": "^1.2.37", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bd3041..95a5529 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,12 @@ importers: .: dependencies: - '@langchain/anthropic': - specifier: ^1.3.25 - version: 1.4.0(@langchain/core@1.1.48) '@langchain/core': - specifier: ^1.1.36 - version: 1.1.48 + specifier: ^1.2.3 + version: 1.2.3(openai@6.49.0(zod@4.4.3)) + '@langchain/openai': + specifier: ^1.5.5 + version: 1.5.5(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3))) '@octokit/rest': specifier: ^22.0.1 version: 22.0.1 @@ -22,7 +22,7 @@ importers: version: 17.4.2 langchain: specifier: ^1.2.37 - version: 1.4.4(@langchain/core@1.1.48) + version: 1.4.4(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3)))(openai@6.49.0(zod@4.4.3)) node-cron: specifier: ^4.2.1 version: 4.2.1 @@ -54,19 +54,6 @@ importers: packages: - '@anthropic-ai/sdk@0.95.2': - resolution: {integrity: sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ==} - hasBin: true - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} - engines: {node: '>=6.9.0'} - '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} @@ -84,14 +71,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@langchain/anthropic@1.4.0': - resolution: {integrity: sha512-rs1yVydrHjyiD31uChdCnKZpmDuKa0Bpz8Raiy9GvqnqmfXPMe0oOrap/2paE+NRSinDbtax8mMpP/yv8EbO1A==} - engines: {node: '>=20'} - peerDependencies: - '@langchain/core': ^1.1.47 - - '@langchain/core@1.1.48': - resolution: {integrity: sha512-fQU6Guyb1pwc2fEplmA8FPbKfOMAofjnyJzExevro0FxEiuGHE18Ov/ZHmT9trWCDTZRI9eW1VIc6aChxV8pAQ==} + '@langchain/core@1.2.3': + resolution: {integrity: sha512-F+L5SsciykwDl7eDxacnhDTcWe1IF6jetzfkvI5PPfq6ogWHO7xcjU90SGh/3lqbbS0tgun+qF01KIqxawrCsA==} engines: {node: '>=20'} '@langchain/langgraph-checkpoint@1.0.4': @@ -129,6 +110,12 @@ packages: zod-to-json-schema: optional: true + '@langchain/openai@1.5.5': + resolution: {integrity: sha512-wX7dwb9z4nf5FHXlIl/X2mk08pzonvRHCt1D4+s1zXLP0duYDC95j7dulPIQJ6fmhbyYQc9Ki8mEhY/D1lB8kw==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.2.2 + '@langchain/protocol@0.0.16': resolution: {integrity: sha512-ws+J7MaHyhO5dG7f0vdyHQiUn9hoCnki0f3crJPa4MCTGzcRC39jYSCghyrGtBPYQnZbUQiGyRVpW3z3M8IpJg==} @@ -184,9 +171,6 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} - '@stablelib/base64@1.0.1': - resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -253,9 +237,6 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - fast-sha256@1.3.0: - resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} - is-network-error@1.3.2: resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==} engines: {node: '>=16'} @@ -263,10 +244,6 @@ packages: js-tiktoken@1.0.21: resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} - json-schema-to-ts@3.1.1: - resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} - engines: {node: '>=16'} - json-with-bigint@3.5.8: resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} @@ -307,6 +284,26 @@ packages: resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==} engines: {node: '>=6.0.0'} + openai@6.49.0: + resolution: {integrity: sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg==} + peerDependencies: + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true + ws: + optional: true + zod: + optional: true + p-finally@1.0.0: resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} engines: {node: '>=4'} @@ -390,12 +387,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - standardwebhooks@1.0.0: - resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} - - ts-algebra@2.0.0: - resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -441,15 +432,6 @@ packages: snapshots: - '@anthropic-ai/sdk@0.95.2(zod@4.4.3)': - dependencies: - json-schema-to-ts: 3.1.1 - standardwebhooks: 1.0.0 - optionalDependencies: - zod: 4.4.3 - - '@babel/runtime@7.29.7': {} - '@cfworker/json-schema@4.1.1': {} '@cspotcode/source-map-support@0.8.1': @@ -465,18 +447,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@langchain/anthropic@1.4.0(@langchain/core@1.1.48)': - dependencies: - '@anthropic-ai/sdk': 0.95.2(zod@4.4.3) - '@langchain/core': 1.1.48 - zod: 4.4.3 - - '@langchain/core@1.1.48': + '@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3))': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 js-tiktoken: 1.0.21 - langsmith: 0.7.5 + langsmith: 0.7.5(openai@6.49.0(zod@4.4.3)) mustache: 4.2.0 p-queue: 6.6.2 zod: 4.4.3 @@ -487,25 +463,25 @@ snapshots: - openai - ws - '@langchain/langgraph-checkpoint@1.0.4(@langchain/core@1.1.48)': + '@langchain/langgraph-checkpoint@1.0.4(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3)))': dependencies: - '@langchain/core': 1.1.48 + '@langchain/core': 1.2.3(openai@6.49.0(zod@4.4.3)) uuid: 14.0.0 - '@langchain/langgraph-sdk@1.9.20(@langchain/core@1.1.48)': + '@langchain/langgraph-sdk@1.9.20(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3)))': dependencies: - '@langchain/core': 1.1.48 + '@langchain/core': 1.2.3(openai@6.49.0(zod@4.4.3)) '@langchain/protocol': 0.0.16 '@types/json-schema': 7.0.15 p-queue: 9.3.0 p-retry: 7.1.1 uuid: 14.0.0 - '@langchain/langgraph@1.3.7(@langchain/core@1.1.48)(zod@4.4.3)': + '@langchain/langgraph@1.3.7(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3)))(zod@4.4.3)': dependencies: - '@langchain/core': 1.1.48 - '@langchain/langgraph-checkpoint': 1.0.4(@langchain/core@1.1.48) - '@langchain/langgraph-sdk': 1.9.20(@langchain/core@1.1.48) + '@langchain/core': 1.2.3(openai@6.49.0(zod@4.4.3)) + '@langchain/langgraph-checkpoint': 1.0.4(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3))) + '@langchain/langgraph-sdk': 1.9.20(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3))) '@langchain/protocol': 0.0.16 '@standard-schema/spec': 1.1.0 uuid: 14.0.0 @@ -516,6 +492,18 @@ snapshots: - svelte - vue + '@langchain/openai@1.5.5(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3)))': + dependencies: + '@langchain/core': 1.2.3(openai@6.49.0(zod@4.4.3)) + js-tiktoken: 1.0.21 + openai: 6.49.0(zod@4.4.3) + zod: 4.4.3 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - '@smithy/hash-node' + - '@smithy/signature-v4' + - ws + '@langchain/protocol@0.0.16': {} '@octokit/auth-token@6.0.0': {} @@ -581,8 +569,6 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 - '@stablelib/base64@1.0.1': {} - '@standard-schema/spec@1.1.0': {} '@tsconfig/node10@1.0.12': {} @@ -631,27 +617,20 @@ snapshots: eventemitter3@5.0.4: {} - fast-sha256@1.3.0: {} - is-network-error@1.3.2: {} js-tiktoken@1.0.21: dependencies: base64-js: 1.5.1 - json-schema-to-ts@3.1.1: - dependencies: - '@babel/runtime': 7.29.7 - ts-algebra: 2.0.0 - json-with-bigint@3.5.8: {} - langchain@1.4.4(@langchain/core@1.1.48): + langchain@1.4.4(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3)))(openai@6.49.0(zod@4.4.3)): dependencies: - '@langchain/core': 1.1.48 - '@langchain/langgraph': 1.3.7(@langchain/core@1.1.48)(zod@4.4.3) - '@langchain/langgraph-checkpoint': 1.0.4(@langchain/core@1.1.48) - langsmith: 0.7.5 + '@langchain/core': 1.2.3(openai@6.49.0(zod@4.4.3)) + '@langchain/langgraph': 1.3.7(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3)))(zod@4.4.3) + '@langchain/langgraph-checkpoint': 1.0.4(@langchain/core@1.2.3(openai@6.49.0(zod@4.4.3))) + langsmith: 0.7.5(openai@6.49.0(zod@4.4.3)) zod: 4.4.3 transitivePeerDependencies: - '@opentelemetry/api' @@ -665,9 +644,11 @@ snapshots: - ws - zod-to-json-schema - langsmith@0.7.5: + langsmith@0.7.5(openai@6.49.0(zod@4.4.3)): dependencies: p-queue: 6.6.2 + optionalDependencies: + openai: 6.49.0(zod@4.4.3) make-error@1.3.6: {} @@ -675,6 +656,10 @@ snapshots: node-cron@4.2.1: {} + openai@6.49.0(zod@4.4.3): + optionalDependencies: + zod: 4.4.3 + p-finally@1.0.0: {} p-queue@6.6.2: @@ -746,13 +731,6 @@ snapshots: split2@4.2.0: {} - standardwebhooks@1.0.0: - dependencies: - '@stablelib/base64': 1.0.1 - fast-sha256: 1.3.0 - - ts-algebra@2.0.0: {} - ts-node@10.9.2(@types/node@25.9.2)(typescript@6.0.3): dependencies: '@cspotcode/source-map-support': 0.8.1 diff --git a/src/agent/cleanup.agent.ts b/src/agent/cleanup.agent.ts index b5a7f15..ba86014 100644 --- a/src/agent/cleanup.agent.ts +++ b/src/agent/cleanup.agent.ts @@ -1,13 +1,10 @@ import { createAgent, toolCallLimitMiddleware } from 'langchain' -import { ChatAnthropic } from '@langchain/anthropic' +import { createLlm } from './llm.js' import { cleanupTool } from '../tools/cleanup.tool.js' import { CLEANUP_PROMPT } from './prompt.js' -import { DEFAULT_LLM } from '../constants/index.js' - -const llm = new ChatAnthropic({ model: DEFAULT_LLM, temperature: 0 }) export const cleanupAgent = createAgent({ - model: llm, + model: createLlm(), tools: [cleanupTool], systemPrompt: CLEANUP_PROMPT, middleware: [toolCallLimitMiddleware({ runLimit: 1, exitBehavior: 'end' })], diff --git a/src/agent/diary.agent.ts b/src/agent/diary.agent.ts index 7266bb3..e535143 100644 --- a/src/agent/diary.agent.ts +++ b/src/agent/diary.agent.ts @@ -1,13 +1,10 @@ import { createAgent, toolCallLimitMiddleware } from 'langchain' -import { ChatAnthropic } from '@langchain/anthropic' +import { createLlm } from './llm.js' import { diaryTool } from '../tools/diary.tool.js' import { DIARY_PROMPT } from './prompt.js' -import { DEFAULT_LLM } from '../constants/index.js' - -const llm = new ChatAnthropic({ model: DEFAULT_LLM, temperature: 0 }) export const diaryAgent = createAgent({ - model: llm, + model: createLlm(), tools: [diaryTool], systemPrompt: DIARY_PROMPT, middleware: [toolCallLimitMiddleware({ runLimit: 1, exitBehavior: 'end' })], diff --git a/src/agent/github.agent.ts b/src/agent/github.agent.ts index 92cfa5f..82cfa6a 100644 --- a/src/agent/github.agent.ts +++ b/src/agent/github.agent.ts @@ -1,13 +1,10 @@ import { createAgent, toolCallLimitMiddleware } from 'langchain' -import { ChatAnthropic } from '@langchain/anthropic' +import { createLlm } from './llm.js' import { githubTool } from '../tools/github.tool.js' import { GITHUB_PROMPT } from './prompt.js' -import { DEFAULT_LLM } from '../constants/index.js' - -const llm = new ChatAnthropic({ model: DEFAULT_LLM, temperature: 0 }) export const githubAgent = createAgent({ - model: llm, + model: createLlm(), tools: [githubTool], systemPrompt: GITHUB_PROMPT, middleware: [toolCallLimitMiddleware({ runLimit: 1, exitBehavior: 'end' })], diff --git a/src/agent/llm.ts b/src/agent/llm.ts new file mode 100644 index 0000000..bb78c13 --- /dev/null +++ b/src/agent/llm.ts @@ -0,0 +1,17 @@ +import { ChatOpenAI } from '@langchain/openai' +import { DEFAULT_LLM, DEFAULT_LLM_BASE_URL } from '../constants/index.js' + +// ponytail: one model factory for every agent. Any OpenAI-compatible provider — +// OpenRouter (default), NVIDIA NIM, a local model — selected by LLM_BASE_URL. +// Named for the protocol rather than a vendor, since the base URL decides where it goes. +export const createLlm = (temperature = 0) => { + const apiKey = process.env.LLM_API_KEY + if (!apiKey) throw new Error('LLM_API_KEY is not set — the agents have no model to call') + + return new ChatOpenAI({ + model: process.env.LLM_MODEL || DEFAULT_LLM, + apiKey, + temperature, + configuration: { baseURL: process.env.LLM_BASE_URL || DEFAULT_LLM_BASE_URL }, + }) +} diff --git a/src/agent/manual-kpi.agent.ts b/src/agent/manual-kpi.agent.ts index d33ac15..a4db5d0 100644 --- a/src/agent/manual-kpi.agent.ts +++ b/src/agent/manual-kpi.agent.ts @@ -1,13 +1,10 @@ import { createAgent, toolCallLimitMiddleware } from 'langchain' -import { ChatAnthropic } from '@langchain/anthropic' +import { createLlm } from './llm.js' import { manualKpiTool } from '../tools/manual-kpi.tool.js' -import { DEFAULT_LLM } from '../constants/index.js' import { MANUAL_PROMPT } from './prompt.js' -const llm = new ChatAnthropic({ model: DEFAULT_LLM, temperature: 0 }) - export const manualKpiAgent = createAgent({ - model: llm, + model: createLlm(), tools: [manualKpiTool], systemPrompt: MANUAL_PROMPT, middleware: [toolCallLimitMiddleware({ runLimit: 1, exitBehavior: 'end' })], diff --git a/src/agent/news-curator.agent.ts b/src/agent/news-curator.agent.ts index 4966db8..e0a86af 100644 --- a/src/agent/news-curator.agent.ts +++ b/src/agent/news-curator.agent.ts @@ -1,13 +1,10 @@ import { createAgent, toolCallLimitMiddleware } from 'langchain' -import { ChatAnthropic } from '@langchain/anthropic' +import { createLlm } from './llm.js' import { trendingCuratorTool } from '../tools/news-curator.tool.js' import { TRENDING_CURATOR_PROMPT } from './prompt.js' -import { DEFAULT_LLM } from '../constants/index.js' - -const llm = new ChatAnthropic({ model: DEFAULT_LLM, temperature: 0.5 }) export const trendingCuratorAgent = createAgent({ - model: llm, + model: createLlm(0.5), tools: [trendingCuratorTool], systemPrompt: TRENDING_CURATOR_PROMPT, middleware: [toolCallLimitMiddleware({ runLimit: 1, exitBehavior: 'end' })], diff --git a/src/agent/news-telegram.agent.ts b/src/agent/news-telegram.agent.ts index 15f0a82..09d7c84 100644 --- a/src/agent/news-telegram.agent.ts +++ b/src/agent/news-telegram.agent.ts @@ -1,13 +1,10 @@ import { createAgent, toolCallLimitMiddleware } from 'langchain' -import { ChatAnthropic } from '@langchain/anthropic' +import { createLlm } from './llm.js' import { trendingTelegramTool } from '../tools/news-telegram.tool.js' import { TRENDING_TELEGRAM_PROMPT } from './prompt.js' -import { DEFAULT_LLM } from '../constants/index.js' - -const llm = new ChatAnthropic({ model: DEFAULT_LLM, temperature: 0 }) export const trendingTelegramAgent = createAgent({ - model: llm, + model: createLlm(), tools: [trendingTelegramTool], systemPrompt: TRENDING_TELEGRAM_PROMPT, middleware: [toolCallLimitMiddleware({ runLimit: 1, exitBehavior: 'end' })], diff --git a/src/agent/prompt.ts b/src/agent/prompt.ts index f36d7db..5e1a844 100644 --- a/src/agent/prompt.ts +++ b/src/agent/prompt.ts @@ -41,15 +41,16 @@ export const CLEANUP_PROMPT = export const MANUAL_PROMPT = 'You are a manual input agent. Your only job is to call collect_manual_kpi_input once to ask the engineer what else they did today, then return the result as-is.' -export const TRENDING_CURATOR_PROMPT = `You are a GitHub trending repos curator for a TypeScript/JavaScript/Node.js developer. You receive a list of scraped trending repos and your job is to select the top 1-3 most interesting ones. +export const TRENDING_CURATOR_PROMPT = `You are a GitHub trending repos curator for a TypeScript/JavaScript/Node.js developer. You receive a list of scraped trending repos and your job is to select the top 5-8 most interesting ones. When curating: - Prioritize repos most relevant to TS/JS/Node developers: frameworks, libraries, dev tools, AI/LLM tooling, build tools, etc. - Include repos in other languages ONLY if they have a direct impact on JS/TS workflows (e.g. a Rust-based bundler, a Go CLI tool for JS devs) -- Write a 1-2 sentence summary for each repo explaining WHY it's interesting and what problem it solves +- Write a 1-2 sentence summary for each repo explaining WHY it's interesting and what problem it solves. Keep it under 140 characters — it is read on a phone +- Do not just restate the repo's own description; say what problem it solves for a TS/JS engineer - Rank by relevance — the most useful repo for a TS/JS engineer should be first -- Preserve all original fields (repo_name, url, description, language, stars, today_stars) and add the summary -- Add 3-5 lowercase tags per repo for classification (e.g. "ai", "framework", "typescript", "bundler", "devtools", "testing", "cli", "database", "ui", "api"). Tags should be consistent across repos — reuse the same tag when the category matches +- Preserve all original fields (repo_name, url, description, language, stars, today_stars) exactly as given — never alter a URL or star count +- Add 3-5 tags per repo, chosen from EXACTLY this list: ai, framework, library, devtools, bundler, testing, cli, database, ui, api, runtime, security, typescript. Do not invent tags outside this list — they are rendered as searchable hashtags, so they must be identical day to day Call curate_trending_repos with the result immediately.` diff --git a/src/constants/index.ts b/src/constants/index.ts index f1076f4..7210f2e 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,4 +1,5 @@ -export const DEFAULT_LLM = 'claude-haiku-4-5' +export const DEFAULT_LLM = 'nvidia/nemotron-3-ultra-550b-a55b:free' +export const DEFAULT_LLM_BASE_URL = 'https://openrouter.ai/api/v1' export const COMPANY_CLEANUP_TABLE = 'mockTestUsers' export const COMPANY_CLEANUP_THRESHOLD_DAYS = '30' export const DEFAULT_CRONJOB_TIME = '0 17 * * *' diff --git a/src/tools/news-curator.tool.ts b/src/tools/news-curator.tool.ts index 52cad1e..664fa75 100644 --- a/src/tools/news-curator.tool.ts +++ b/src/tools/news-curator.tool.ts @@ -14,8 +14,12 @@ export const trendingCuratorTool = new DynamicStructuredTool({ language: z.string().describe('Primary language'), stars: z.number().describe('Total star count'), today_stars: z.number().describe('Stars gained today'), - summary: z.string().describe('1-2 sentence summary of why this repo is interesting for a TS/JS developer'), - tags: z.array(z.string()).describe('3-5 lowercase tags for classification e.g. ["ai", "framework", "typescript", "bundler", "devtools"]'), + summary: z.string().describe('1-2 sentence summary, under 140 characters, of why this repo is interesting for a TS/JS developer'), + tags: z + .array(z.string()) + .describe( + '3-5 tags chosen only from: ai, framework, library, devtools, bundler, testing, cli, database, ui, api, runtime, security, typescript' + ), }) ) .describe('Top 5-8 curated trending repos, ranked by relevance'), diff --git a/src/tools/news-telegram.tool.ts b/src/tools/news-telegram.tool.ts index 1131855..af79305 100644 --- a/src/tools/news-telegram.tool.ts +++ b/src/tools/news-telegram.tool.ts @@ -36,12 +36,15 @@ export const trendingTelegramTool = new DynamicStructuredTool({ const repoLines = repos.map((r, i) => { const num = numberEmojis[i] ?? `${i + 1}.` + // ponytail: the prompt asks for <140 chars, this is what actually guarantees it. + // Telegram hard-fails the whole message past 4096, so the bound lives in code. + const summary = r.summary.length > 140 ? `${r.summary.slice(0, 137)}...` : r.summary return [ `${num} ${escapeHtml(r.repo_name)}`, `⭐ ${r.stars.toLocaleString()} (+${r.today_stars} today) · ${escapeHtml(r.language)}`, - `${escapeHtml(r.summary)}`, - `🏷 ${r.tags.map((t) => `#${t}`).join(' ')}`, - `🔗 View on GitHub`, + `${escapeHtml(summary)}`, + `🏷 ${r.tags.map((t) => `#${escapeHtml(t)}`).join(' ')}`, + `🔗 View on GitHub`, ].join('\n') }) diff --git a/src/tools/trending-scrape.tool.ts b/src/tools/trending-scrape.tool.ts index b3c942c..a1ea9c8 100644 --- a/src/tools/trending-scrape.tool.ts +++ b/src/tools/trending-scrape.tool.ts @@ -31,8 +31,9 @@ function parseTrendingHtml(html: string): TrendingRepo[] { const langMatch = block.match(/([\s\S]*?)<\/span>/) const language = langMatch ? langMatch[1].trim() : '' - // Total stars — matches the first standalone star count (not the "stars today" one) - const starsMatch = block.match(/href="\/[^"]*\/stargazers"[^>]*>\s*([\d,]+)\s*<\/a>/) + // Total stars — the count sits after the star icon inside the stargazers link, + // so we have to skip the icon markup rather than read straight after the opening tag. + const starsMatch = block.match(/href="\/[^"]*\/stargazers"[\s\S]*?<\/svg>\s*([\d,]+)/) const stars = starsMatch ? parseInt(starsMatch[1].replace(/,/g, ''), 10) : 0 // Stars today From be2336b11c6b256a426bd73309ab303c5885b829 Mon Sep 17 00:00:00 2001 From: damengrandom Date: Tue, 28 Jul 2026 17:38:09 +1000 Subject: [PATCH 2/2] fix(T22): bump Fallow action to v3 The v2 action verifies `fallow-lsp` and `fallow-mcp` binaries, but from fallow 3.x the platform packages ship a single multicall `fallow` binary. Since the action installs the latest CLI by default, every run failed with `binary-missing`. v3's verifier expects only `fallow`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CFr2cPw9xvDvStaya9sDW8 --- .github/workflows/code-quality.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 1dad9cb..829f18c 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -71,7 +71,10 @@ jobs: fetch-depth: 0 # full history so Fallow's change-hotspot analysis works - name: Run Fallow analysis - uses: fallow-rs/fallow@v2 + # v3+: platform packages ship a single multicall `fallow` binary. The v2 + # action still verified `fallow-lsp`/`fallow-mcp`, which no published CLI + # provides any more, so every run failed at binary verification. + uses: fallow-rs/fallow@v3 with: # Advisory mode: still annotate the PR with findings, but don't fail # the check on warnings/issues (default for this input is `true`).