├── .gitignore ├── src ├── index.ts ├── app.ts ├── utils.test.ts ├── mention.ts ├── mention.test.ts └── utils.ts ├── jest.config.js ├── manifests ├── service.yml └── deployment.yml ├── tsconfig.json ├── Dockerfile ├── .eslintrc.js ├── README.md ├── .github └── workflows │ ├── ci.yml │ └── build_image.yml ├── manifest.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { app } from './app' 2 | import { appMention } from './mention'; 3 | 4 | (async () => { 5 | // アプリを起動します 6 | await app.start(process.env.PORT || 3000) 7 | })() 8 | app.event('app_mention', appMention) 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "testMatch": [ 3 | "**/__tests__/**/*.+(ts|tsx|js)", 4 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 5 | ], 6 | "transform": { 7 | "^.+\\.(ts|tsx)$": "ts-jest" 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /manifests/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: slack-gpt 5 | namespace: default 6 | spec: 7 | selector: 8 | name: slack-gpt 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | targetPort: 3000 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "skipLibCheck": true, 8 | "outDir": "./dist", 9 | "resolveJsonModule": true 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:current-alpine3.17 2 | RUN apk update && apk upgrade 3 | COPY package*.json ./ 4 | 5 | RUN addgroup -S slack-gpt && adduser -S slack-gpt -G slack-gpt 6 | RUN mkdir -p /app && chown -R slack-gpt /app 7 | WORKDIR /app 8 | RUN npm install 9 | EXPOSE 3000 10 | COPY --chown=slack-gpt:slack-gpt . . 11 | USER slack-gpt 12 | CMD [ "npm", "start" ] 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: [ 7 | 'standard', 8 | 'plugin:jest/recommended', 9 | 'plugin:jest/style' 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaVersion: 'latest', 14 | sourceType: 'module' 15 | }, 16 | plugins: [ 17 | '@typescript-eslint', 18 | 'jest' 19 | ], 20 | rules: { 21 | camelcase: 'off' 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { App, LogLevel } from '@slack/bolt' 2 | import * as dotenv from 'dotenv' 3 | dotenv.config() 4 | 5 | export const app = new App({ 6 | token: process.env.SLACK_BOT_TOKEN, 7 | signingSecret: process.env.SLACK_SIGNING_SECRET, 8 | logLevel: LogLevel.INFO, 9 | customRoutes: [ 10 | { 11 | path: '/health-check', 12 | method: ['GET'], 13 | handler: (req, res) => { 14 | res.writeHead(200) 15 | res.end('Health check information displayed here!') 16 | } 17 | } 18 | ] 19 | }) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Slack-GPT 2 | 3 | SlackショートカットもしくはメンションからGPT4のAPIを呼び出し、応答するSlackボットです。メンションして話しかけ続けることでコンテキストを保ったやり取りが可能です。 4 | 5 | ## Deploy 6 | 7 | 1. app manifestをSlackにDeployする。(manifest.json)その際に、example.comとなっている箇所はURLを書き換えてください。 8 | 2. アプリをk8sなどで実行する。exampleディレクトリにサンプルマニフェストがあります。ingressはよしなにやってください。 9 | 3. secretの登録 10 | 11 | ```bash 12 | $ kubectl create secret generic -n default slack-gpt \ 13 | --from-literal=slack-bot-token=xxx \ 14 | --from-literal=slack-signing-secret=xxx \ 15 | --from-literal=openai-secret=xxx 16 | ``` 17 | ## Author 18 | - pyama86 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: TypeScript Test and Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test_and_lint: 13 | runs-on: ubuntu-latest 14 | env: 15 | OPENAI_API_KEY: "dummy" 16 | OPENAI_MAX_TOKENS: 1000 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: 17.x 25 | cache: 'npm' 26 | 27 | - name: Install dependencies 28 | run: npm ci 29 | 30 | - name: Run Jest tests 31 | run: npm test 32 | 33 | - name: Run ESLint lint 34 | run: npm run lint 35 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_information": { 3 | "name": "slack-gpt" 4 | }, 5 | "features": { 6 | "bot_user": { 7 | "display_name": "slack-gpt", 8 | "always_online": false 9 | }, 10 | "shortcuts": [ 11 | { 12 | "name": "GPTに聞いてみる", 13 | "type": "message", 14 | "callback_id": "query", 15 | "description": "Slackの発言を元にGPTに聞いてみます" 16 | } 17 | ] 18 | }, 19 | "oauth_config": { 20 | "scopes": { 21 | "bot": [ 22 | "channels:history", 23 | "channels:read", 24 | "chat:write", 25 | "chat:write.public", 26 | "commands", 27 | "app_mentions:read" 28 | ] 29 | } 30 | }, 31 | "settings": { 32 | "event_subscriptions": { 33 | "request_url": "https://example.com/slack/events", 34 | "bot_events": [ 35 | "app_mention" 36 | ] 37 | }, 38 | "interactivity": { 39 | "is_enabled": true, 40 | "request_url": "https://example.com/slack/events" 41 | }, 42 | "org_deploy_enabled": false, 43 | "socket_mode_enabled": false, 44 | "token_rotation_enabled": false 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@dqbd/tiktoken": "^1.0.7", 4 | "@slack/bolt": "^3.14.0", 5 | "@slack/web-api": "^6.9.1", 6 | "@types/jest": "^29.5.7", 7 | "axios": "^1.6.0", 8 | "dotenv": "^16.3.1", 9 | "jest": "^29.7.0", 10 | "jest-mock-extended": "^3.0.5", 11 | "node-cache": "^5.1.2", 12 | "openai": "^4.16.1", 13 | "supertest": "^6.3.3", 14 | "tiktoken": "^1.0.10", 15 | "ts-jest": "^29.1.1" 16 | }, 17 | "name": "slack-gpt", 18 | "version": "1.0.0", 19 | "main": "index.js", 20 | "repository": "ssh://git@github.com/pyama86/slack-gpt3.git", 21 | "author": "pyama ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@types/node": "^20.8.10", 25 | "@typescript-eslint/eslint-plugin": "^6.10.0", 26 | "@typescript-eslint/parser": "^6.10.0", 27 | "eslint": "^8.53.0", 28 | "eslint-config-standard": "^17.1.0", 29 | "eslint-plugin-import": "^2.29.0", 30 | "eslint-plugin-jest": "^27.6.0", 31 | "eslint-plugin-n": "^16.2.0", 32 | "eslint-plugin-promise": "^6.1.1", 33 | "nodemon": "^3.0.1", 34 | "ts-node": "^10.9.1", 35 | "typescript": "^5.2.2" 36 | }, 37 | "scripts": { 38 | "lint": "eslint src --ext .ts", 39 | "test": "OPENAI_API_KEY=dummy_api_key jest", 40 | "start": "npm run build:live", 41 | "build": "tsc -p .", 42 | "build:live": "nodemon --watch 'src/**/*.ts' --exec \"ts-node\" src/index.ts" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/build_image.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | on: 3 | schedule: 4 | - cron: "0 10 * * 1" 5 | push: 6 | branches: 7 | - "main" 8 | tags: 9 | - "v*.*.*" 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - 15 | name: Checkout 16 | uses: actions/checkout@v3 17 | - 18 | name: Docker meta 19 | id: meta 20 | uses: docker/metadata-action@v4 21 | with: 22 | images: | 23 | ghcr.io/pyama86/slack-gpt 24 | tags: | 25 | type=schedule 26 | type=ref,event=branch 27 | type=ref,event=pr 28 | type=semver,pattern={{version}} 29 | type=semver,pattern={{major}}.{{minor}} 30 | type=semver,pattern={{major}} 31 | type=sha 32 | - 33 | name: Set up QEMU 34 | uses: docker/setup-qemu-action@v2 35 | - 36 | name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v2 38 | - 39 | name: Login to GHCR 40 | uses: docker/login-action@v2 41 | with: 42 | registry: ghcr.io 43 | username: ${{ github.repository_owner }} 44 | password: ${{ secrets.GITHUB_TOKEN }} 45 | - 46 | name: Build and push 47 | uses: docker/build-push-action@v3 48 | with: 49 | context: . 50 | push: true 51 | tags: ${{ steps.meta.outputs.tags }} 52 | labels: ${{ steps.meta.outputs.labels }} 53 | -------------------------------------------------------------------------------- /manifests/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: slack-gpt 5 | namespace: default 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: slack-gpt 11 | template: 12 | metadata: 13 | labels: 14 | name: slack-gpt 15 | spec: 16 | containers: 17 | - name: slack-gpt 18 | imagePullPolicy: Always 19 | image: ghcr.io/pyama86/slack-gpt:sha-de66c25 20 | ports: 21 | - containerPort: 3000 22 | livenessProbe: 23 | initialDelaySeconds: 10 24 | periodSeconds: 10 25 | tcpSocket: 26 | port: 3000 27 | readinessProbe: 28 | initialDelaySeconds: 10 29 | periodSeconds: 10 30 | tcpSocket: 31 | port: 3000 32 | env: 33 | - name: SLACK_BOT_TOKEN 34 | valueFrom: 35 | secretKeyRef: 36 | name: slack-gpt 37 | key: slack-bot-token 38 | - name: SLACK_SIGNING_SECRET 39 | valueFrom: 40 | secretKeyRef: 41 | name: slack-gpt 42 | key: slack-signing-secret 43 | - name: OPENAI_API_KEY 44 | valueFrom: 45 | secretKeyRef: 46 | name: slack-gpt 47 | key: openai-secret 48 | - name: GPT_MODEL 49 | value: gpt-4o 50 | - name: LANG 51 | value: C.UTF-8 52 | - name: BOT_USER_ID 53 | value: your-slack-bot-id 54 | -------------------------------------------------------------------------------- /src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from 'openai' 2 | import { generateSummary } from './utils' 3 | 4 | jest.mock('openai', () => { 5 | const createMock = jest.fn().mockResolvedValue({ 6 | choices: [ 7 | { 8 | message: { 9 | content: 'Mocked response content' 10 | } 11 | } 12 | ] 13 | }) 14 | 15 | return { 16 | OpenAI: jest.fn().mockImplementation(() => ({ 17 | chat: { 18 | completions: { 19 | create: createMock 20 | } 21 | } 22 | })) 23 | } 24 | }) 25 | 26 | describe('generateSummary', () => { 27 | let openaiInstance: any 28 | 29 | beforeEach(() => { 30 | jest.clearAllMocks() 31 | openaiInstance = new OpenAI({ apiKey: 'dummy_api_key' }) 32 | }) 33 | 34 | it('should generate a summary from the given messages', async () => { 35 | const messages = [ 36 | { 37 | role: 'user' as const, 38 | content: [ 39 | { 40 | text: 'This is the first message.', 41 | type: 'text' as const 42 | } 43 | ] 44 | }, 45 | { 46 | role: 'user' as const, 47 | content: [ 48 | { 49 | text: 'This is the second message.', 50 | type: 'text' as const 51 | } 52 | ] 53 | } 54 | ] 55 | 56 | const model = 'gpt-4o' 57 | const maxTokens: number = 4096 58 | 59 | const summary = await generateSummary(messages, model, maxTokens) 60 | 61 | expect(summary).toBe('Mocked response content') 62 | 63 | expect(openaiInstance.chat.completions.create).toHaveBeenCalledTimes(1) 64 | expect(openaiInstance.chat.completions.create).toHaveBeenCalledWith({ 65 | model, 66 | messages: expect.any(Array), 67 | max_tokens: maxTokens 68 | }) 69 | }) 70 | 71 | it('should handle empty messages gracefully', async () => { 72 | const messages: any[] = [] 73 | 74 | const model = 'gpt-4o' 75 | const maxTokens: number = 4096 76 | 77 | const summary = await generateSummary(messages, model, maxTokens) 78 | 79 | expect(summary).toBe('Mocked response content') 80 | 81 | expect(openaiInstance.chat.completions.create).toHaveBeenCalledTimes(1) 82 | expect(openaiInstance.chat.completions.create).toHaveBeenCalledWith({ 83 | model, 84 | messages: expect.any(Array), 85 | max_tokens: maxTokens 86 | }) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /src/mention.ts: -------------------------------------------------------------------------------- 1 | import { ask, downloadFileAsBase64, generateSummary } from './utils' 2 | 3 | export const appMention: any = async ({ event, client, say }) => { 4 | const channelId = event.channel 5 | const botUserId = process.env.BOT_USER_ID 6 | try { 7 | const replies = await client.conversations.replies({ 8 | channel: channelId, 9 | ts: event.thread_ts || event.ts 10 | }) 11 | 12 | if (!replies.messages) { 13 | await say('スレッドが見つかりませんでした') 14 | return 15 | } 16 | 17 | const nonNullable = (value: T): value is NonNullable => value != null 18 | let model = process.env.GPT_MODEL || 'gpt-4o' 19 | let max_tokens = null 20 | 21 | const isSummaryRequest = event.text.includes('今北産業') 22 | 23 | const aiInstructionMessage = { 24 | role: 'system', 25 | content: [ 26 | { text: '応答はマークダウンで行ってください。', type: 'text' } 27 | ] 28 | } 29 | 30 | const threadMessages = [ 31 | aiInstructionMessage, 32 | ...(await Promise.all( 33 | replies.messages.map(async (message) => { 34 | if ( 35 | (!isSummaryRequest && message.user !== botUserId && !message.text.includes(`<@${botUserId}>`)) || 36 | (isSummaryRequest && message.text.includes('今北産業')) 37 | ) { 38 | return null 39 | } 40 | 41 | const contents = [] 42 | 43 | if (message.files) { 44 | for (const file of message.files) { 45 | if ('url_private_download' in file) { 46 | const encodedImage = await downloadFileAsBase64(file.url_private_download) 47 | let filetype = file.filetype 48 | if (filetype === 'jpg') { 49 | filetype = 'jpeg' 50 | } 51 | 52 | if (encodedImage) { 53 | model = 'gpt-4o' 54 | max_tokens = 4096 55 | contents.push({ 56 | image_url: { 57 | url: 'data:image/' + filetype + ';base64,' + encodedImage, 58 | detail: 'auto' 59 | }, 60 | type: 'image_url' 61 | }) 62 | } 63 | } 64 | } 65 | } 66 | 67 | contents.push({ text: (message.text || '').replace(`<@${botUserId}>`, ''), type: 'text' }) 68 | return { 69 | role: message.user === botUserId ? 'assistant' : 'user', 70 | content: contents 71 | } 72 | }) 73 | )) 74 | ] 75 | 76 | let answer = '' 77 | if (isSummaryRequest) { 78 | answer = await generateSummary(threadMessages.filter(nonNullable), model, max_tokens) 79 | } else { 80 | answer = await ask(threadMessages.filter(nonNullable), model, max_tokens) 81 | } 82 | 83 | await say({ 84 | thread_ts: event.ts, 85 | text: answer, 86 | type: 'mrkdwn' 87 | }) 88 | } catch (error) { 89 | console.error(error) 90 | await say({ 91 | text: 'なにかエラーが発生しました。開発者に連絡してください。error:' + error, 92 | thread_ts: event.ts 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/mention.test.ts: -------------------------------------------------------------------------------- 1 | import { appMention } from './mention' 2 | 3 | import { ask, generateSummary } from './utils' 4 | 5 | const mockSay = jest.fn() 6 | const mockClient = { 7 | conversations: { 8 | replies: jest.fn(), 9 | members: jest.fn(), 10 | info: jest.fn() 11 | } 12 | } 13 | const mockEvent = { 14 | channel: 'test_channel', 15 | thread_ts: 'test_thread_ts', 16 | ts: 'test_ts', 17 | text: 'test_message' 18 | } 19 | 20 | jest.mock('./utils', () => { 21 | return { 22 | ask: jest.fn(), 23 | generateSummary: jest.fn() 24 | } 25 | }) 26 | 27 | describe('appMention', () => { 28 | beforeEach(() => { 29 | mockClient.conversations.replies.mockClear() 30 | mockSay.mockClear() 31 | ;(ask as jest.Mock).mockClear() 32 | ;(generateSummary as jest.Mock).mockClear() 33 | }) 34 | 35 | it('should handle app mention event', async () => { 36 | mockClient.conversations.replies.mockResolvedValueOnce({ 37 | messages: [ 38 | { 39 | text: 'test message', 40 | user: 'test_user' 41 | } 42 | ] 43 | }) 44 | ;(ask as jest.Mock).mockResolvedValueOnce('GPTの回答') 45 | 46 | const args = { 47 | event: mockEvent as any, 48 | client: mockClient as any, 49 | say: mockSay 50 | } 51 | 52 | await appMention(args) 53 | 54 | expect(mockClient.conversations.replies).toHaveBeenCalledWith({ 55 | channel: 'test_channel', 56 | ts: 'test_thread_ts' 57 | }) 58 | 59 | expect(mockSay).toHaveBeenCalledTimes(1) 60 | 61 | expect(mockSay).toHaveBeenNthCalledWith(1, { 62 | type: 'mrkdwn', 63 | text: 'GPTの回答', 64 | thread_ts: 'test_ts' 65 | }) 66 | }) 67 | 68 | it('should handle app mention event with 今北産業', async () => { 69 | const eventWithSummaryRequest = { 70 | ...mockEvent, 71 | text: '今北産業' 72 | } 73 | 74 | mockClient.conversations.replies.mockResolvedValueOnce({ 75 | messages: [ 76 | { 77 | text: 'test message 1', 78 | user: 'test_user_1' 79 | }, 80 | { 81 | text: 'test message 2', 82 | user: 'test_user_2' 83 | } 84 | ] 85 | }) 86 | ;(generateSummary as jest.Mock).mockResolvedValueOnce('サマリの内容') 87 | 88 | const args = { 89 | event: eventWithSummaryRequest as any, 90 | client: mockClient as any, 91 | say: mockSay 92 | } 93 | 94 | await appMention(args) 95 | 96 | expect(mockClient.conversations.replies).toHaveBeenCalledWith({ 97 | channel: 'test_channel', 98 | ts: 'test_thread_ts' 99 | }) 100 | 101 | expect(generateSummary).toHaveBeenCalledWith( 102 | [ 103 | { 104 | role: 'system', 105 | content: [{ text: '応答はマークダウンで行ってください。', type: 'text' }] 106 | }, 107 | { 108 | role: 'user', 109 | content: [{ text: 'test message 1', type: 'text' }] 110 | }, 111 | { 112 | role: 'user', 113 | content: [{ text: 'test message 2', type: 'text' }] 114 | } 115 | ], 116 | 'gpt-4o', 117 | null 118 | ) 119 | 120 | expect(mockSay).toHaveBeenCalledTimes(1) 121 | expect(mockSay).toHaveBeenNthCalledWith(1, { 122 | type: 'mrkdwn', 123 | text: 'サマリの内容', 124 | thread_ts: 'test_ts' 125 | }) 126 | }) 127 | }) 128 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from 'openai' 2 | import { encoding_for_model, TiktokenModel } from '@dqbd/tiktoken' 3 | import axios from 'axios' 4 | import { 5 | ChatCompletionUserMessageParam 6 | } from 'openai/resources/chat' 7 | const apiKey = process.env.OPENAI_API_KEY as string 8 | if (!apiKey) { 9 | throw new Error('OPENAI_API_KEY is not set') 10 | } 11 | const openai = new OpenAI({ apiKey }) 12 | 13 | const GPT_MODEL = process.env.GPT_MODEL || 'gpt-4-turbo' 14 | 15 | const GPT_MAX_TOKEN = 128000 16 | 17 | async function getNumberOfTokens (messages: ChatCompletionUserMessageParam[]): Promise { 18 | let length = 0 19 | const model = 'gpt-4' as TiktokenModel 20 | 21 | const encoding = encoding_for_model(model) 22 | for (const message of messages) { 23 | if (message.role === 'user') { 24 | for (const content of message.content as any[]) { 25 | if (content.type === 'text') { 26 | length += encoding.encode(content.text).length 27 | } else if (content.type === 'image_url') { 28 | length += 4096 29 | } 30 | } 31 | } 32 | } 33 | encoding.free() 34 | return length 35 | } 36 | 37 | export async function ask (messages: ChatCompletionUserMessageParam[], model: string = GPT_MODEL, max_tokens: number | null = null) { 38 | const numberOfTokens = await getNumberOfTokens(messages) 39 | 40 | if (numberOfTokens > GPT_MAX_TOKEN) { 41 | return 'GPTの制限により、返答できませんでした。' 42 | } 43 | 44 | console.log(model) 45 | console.log('numberOfTokens', numberOfTokens) 46 | 47 | const response = await openai.chat.completions.create({ 48 | model, 49 | messages, 50 | max_tokens 51 | }) 52 | 53 | return response.choices[0].message?.content 54 | } 55 | 56 | export async function downloadFileAsBase64 (url: string) { 57 | try { 58 | const response = await axios.get(url, { 59 | responseType: 'arraybuffer', 60 | headers: { 61 | Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}` 62 | } 63 | }) 64 | 65 | const data: string = Buffer.from(response.data, 'binary').toString('base64') 66 | return data 67 | } catch (error) { 68 | console.error('Error downloading file:', error) 69 | return null 70 | } 71 | } 72 | 73 | export async function generateSummary (messages: ChatCompletionUserMessageParam[], model: string = GPT_MODEL, max_tokens: number | null = null) { 74 | const allMessages = messages 75 | .map(message => { 76 | if (typeof message.content === 'string') { 77 | return message.content 78 | } else { 79 | return message.content 80 | .map(content => { 81 | if ('text' in content) { 82 | return content.text 83 | } else { 84 | return '' 85 | } 86 | }) 87 | .join('') 88 | } 89 | }) 90 | .join('\n') 91 | 92 | const summaryPrompt = ` 93 | 次の内容を要約し、以下の要件を満たしたサマリを作成してください。 94 | 95 | 1. 最初に箇条書き3行で全体の内容を要約して、3行まとめというヘッダを付けてください。その後に詳細な内容を記載してください。 96 | 2. 納期やスケジュールについては明確に記載する 97 | 3. 重要な内容については抜け漏れなく記載する 98 | 4. あとから見た人がすぐに状況を把握できるようにする 99 | 5. 最初に、「AIによるサマリ」と明記する 100 | 101 | ---スレッドの内容--- 102 | ${allMessages} 103 | 104 | ---サマリ--- 105 | ` 106 | const summary = await ask([{ role: 'user', content: [{ type: 'text', text: summaryPrompt }] }], model, max_tokens) 107 | 108 | return summary 109 | } 110 | --------------------------------------------------------------------------------