mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 13:11:49 +00:00
cody-slack: #ask-cody context and GPT-4 streaming (#51194)
⚠️ This PR changes code only inside of the `cody-slack` package. All the other client packages are untouched. I'll be moving Cody Slack to GCP, so I need to merge the PR with [functionality](https://sourcegraph.slack.com/archives/C89KCDK5J/p1682506053493149) before that: - [#ask-cody](https://sourcegraph.slack.com/archives/C04MSD3DP5L) Special Context: Struggling to find info on Cody across various sources? Worry no more! When you ask [@cody_dev](https://sourcegraph.slack.com/team/U051K8MBM7F) a question in the [#ask-cody](https://sourcegraph.slack.com/archives/C04MSD3DP5L) channel, it now searches Cody-notice, developer docs, the handbook, and the sg/sg codebase to provide the best possible answer. 🔍 - Files Used Section: [@cody_dev](https://sourcegraph.slack.com/team/U051K8MBM7F) will now share links to all the files it "used" while answering your questions. This means you can easily verify the information and explore related resources! 📁 - Slack Markdown Support: Answers are now beautifully formatted and compatible with GitHub-flavored markdown. Enjoy a more readable and visually appealing experience! ✨ - Powered by GPT-4: I've updated [@cody_dev](https://sourcegraph.slack.com/team/U051K8MBM7F) to use GPT-4 for better reasoning capabilities and an enhanced understanding of Slack conversations. Get ready for more accurate and insightful answers!
This commit is contained in:
parent
47d37bac26
commit
753ef33f15
@ -9,6 +9,7 @@ module.exports = {
|
||||
},
|
||||
overrides: baseConfig.overrides,
|
||||
rules: {
|
||||
'ban/ban': 'off',
|
||||
'id-length': 'off',
|
||||
'no-console': 'off',
|
||||
'no-restricted-imports': [
|
||||
|
||||
5
client/cody-slack/BUILD.bazel
generated
5
client/cody-slack/BUILD.bazel
generated
@ -26,6 +26,8 @@ ts_project(
|
||||
"src/constants.ts",
|
||||
"src/mention-handler.ts",
|
||||
"src/services/codebase-context.ts",
|
||||
"src/services/github-client.ts",
|
||||
"src/services/local-vector-store.ts",
|
||||
"src/services/openai-completions-client.ts",
|
||||
"src/services/sourcegraph-client.ts",
|
||||
"src/services/stream-completions.ts",
|
||||
@ -39,10 +41,13 @@ ts_project(
|
||||
":node_modules/@slack/bolt",
|
||||
":node_modules/@slack/web-api",
|
||||
":node_modules/@sourcegraph/cody-shared",
|
||||
":node_modules/langchain",
|
||||
":node_modules/openai",
|
||||
":node_modules/slackify-markdown",
|
||||
"//:node_modules/@types/lodash",
|
||||
"//:node_modules/envalid",
|
||||
"//:node_modules/lodash",
|
||||
"//:node_modules/octokit",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@ -9,15 +9,21 @@
|
||||
"start": "ts-node-transpile-only ./src/app.ts",
|
||||
"lint": "pnpm run lint:js",
|
||||
"lint:js": "eslint --cache '**/*.[tj]s?(x)'",
|
||||
"build": "esbuild ./src/app.ts --bundle --outfile=dist/app.js --format=cjs --platform=node",
|
||||
"build": "esbuild ./src/app.ts --bundle --outfile=dist/app.js --external:hnswlib-node --format=cjs --platform=node",
|
||||
"build-ts": "tsc -b --emitDeclarationOnly",
|
||||
"release": "pnpm run build && cd dist && git add . && git commit -m wip && git push heroku master"
|
||||
"release": "pnpm run build && cd dist && git add . && git commit -m wip && git push heroku master",
|
||||
"build:gcp": "esbuild ./src/app.ts --bundle --outfile=package/app.js --external:hnswlib-node --format=cjs --platform=node",
|
||||
"release:gcp": "pnpm run build && cd package && git add . && git commit -m wip && git push gcp main"
|
||||
},
|
||||
"dependencies": {
|
||||
"@slack/bolt": "^3.12.2",
|
||||
"@slack/web-api": "^6.8.1",
|
||||
"@sourcegraph/cody-shared": "workspace:*",
|
||||
"@sourcegraph/common": "workspace:*",
|
||||
"openai": "^3.2.1"
|
||||
"axios": "^1.3.6",
|
||||
"hnswlib-node": "^1.4.2",
|
||||
"langchain": "^0.0.61",
|
||||
"openai": "^3.2.1",
|
||||
"slackify-markdown": "^4.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
14
client/cody-slack/package/package.json
Normal file
14
client/cody-slack/package/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@sourcegraph/cody-slack",
|
||||
"private": true,
|
||||
"displayName": "Sourcegraph Cody Slack",
|
||||
"version": "0.0.1",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Your programming sidekick powered by AI and Sourcegraph's code search and intelligence.",
|
||||
"scripts": {
|
||||
"start": "node ./app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"hnswlib-node": "^1.4.2"
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,31 @@
|
||||
import { ENVIRONMENT_CONFIG, DEFAULT_APP_SETTINGS } from './constants'
|
||||
import { ENVIRONMENT_CONFIG, DEFAULT_CODEBASES, AppContext, DEFAULT_APP_SETTINGS } from './constants'
|
||||
import { handleHumanMessage } from './mention-handler'
|
||||
import { createCodebaseContext } from './services/codebase-context'
|
||||
import { getVectorStore } from './services/local-vector-store'
|
||||
import { isBotEvent } from './slack/helpers'
|
||||
import { app } from './slack/init'
|
||||
|
||||
const { PORT } = ENVIRONMENT_CONFIG
|
||||
|
||||
async function createAppContext() {
|
||||
// Init codebase context clients for specified Slack channels.
|
||||
const appContext = { codebaseContexts: {} } as AppContext
|
||||
for (const codebase of DEFAULT_CODEBASES) {
|
||||
appContext.codebaseContexts[codebase] = await createCodebaseContext(
|
||||
codebase,
|
||||
DEFAULT_APP_SETTINGS.contextType,
|
||||
DEFAULT_APP_SETTINGS.serverEndpoint
|
||||
)
|
||||
}
|
||||
|
||||
appContext.vectorStore = await getVectorStore()
|
||||
|
||||
return appContext
|
||||
}
|
||||
|
||||
// Main function to start the bot
|
||||
async function startBot() {
|
||||
// Create a context for the codebase using the default app settings
|
||||
const codebaseContext = await createCodebaseContext(
|
||||
DEFAULT_APP_SETTINGS.codebase,
|
||||
DEFAULT_APP_SETTINGS.contextType,
|
||||
DEFAULT_APP_SETTINGS.serverEndpoint
|
||||
)
|
||||
const appContext = await createAppContext()
|
||||
|
||||
// Listen for mentions in the Slack app
|
||||
app.event<'app_mention'>('app_mention', async ({ event }) => {
|
||||
@ -22,9 +34,9 @@ async function startBot() {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('APP_MENTION', event.text)
|
||||
console.log('APP_MENTION:', event.text)
|
||||
// Process the mention event generated by a human user
|
||||
await handleHumanMessage(event, codebaseContext)
|
||||
await handleHumanMessage(event, appContext)
|
||||
})
|
||||
|
||||
// Start the Slack app on the specified port
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import { cleanEnv, str, num } from 'envalid'
|
||||
import { HNSWLib } from 'langchain/vectorstores/hnswlib'
|
||||
|
||||
import { CodebaseContext } from '@sourcegraph/cody-shared/src/codebase-context'
|
||||
|
||||
export const ENVIRONMENT_CONFIG = cleanEnv(process.env, {
|
||||
PORT: num({ default: 3000 }),
|
||||
@ -6,14 +9,27 @@ export const ENVIRONMENT_CONFIG = cleanEnv(process.env, {
|
||||
// OPENAI_API_KEY: str(),
|
||||
SOURCEGRAPH_ACCESS_TOKEN: str(),
|
||||
|
||||
GITHUB_TOKEN: str(),
|
||||
|
||||
SLACK_APP_TOKEN: str(),
|
||||
SLACK_BOT_TOKEN: str(),
|
||||
SLACK_SIGNING_SECRET: str(),
|
||||
})
|
||||
|
||||
export const DEFAULT_APP_SETTINGS = {
|
||||
codebase: 'github.com/sourcegraph/sourcegraph',
|
||||
serverEndpoint: 'https://sourcegraph.sourcegraph.com',
|
||||
contextType: 'blended',
|
||||
debug: 'development',
|
||||
contextType: 'blended',
|
||||
} as const
|
||||
|
||||
export const DEFAULT_CODEBASES = [
|
||||
'github.com/sourcegraph/sourcegraph',
|
||||
'github.com/sourcegraph/handbook',
|
||||
'github.com/sourcegraph/about',
|
||||
] as const
|
||||
|
||||
export type CodebaseContexts = Record<typeof DEFAULT_CODEBASES[number], CodebaseContext>
|
||||
export interface AppContext {
|
||||
codebaseContexts: CodebaseContexts
|
||||
vectorStore: HNSWLib
|
||||
}
|
||||
|
||||
@ -4,28 +4,41 @@ import { throttle } from 'lodash'
|
||||
|
||||
import { Transcript } from '@sourcegraph/cody-shared/src/chat/transcript'
|
||||
import { reformatBotMessage } from '@sourcegraph/cody-shared/src/chat/viewHelpers'
|
||||
import { CodebaseContext } from '@sourcegraph/cody-shared/src/codebase-context'
|
||||
import { Message as PromptMessage } from '@sourcegraph/cody-shared/src/sourcegraph-api'
|
||||
|
||||
import { intentDetector } from './services/sourcegraph-client'
|
||||
import { AppContext } from './constants'
|
||||
import { streamCompletions } from './services/stream-completions'
|
||||
import * as slackHelpers from './slack/helpers'
|
||||
import { interactionFromMessage } from './slack/message-interaction'
|
||||
import { cleanupMessageForPrompt, getSlackInteraction } from './slack/message-interaction'
|
||||
import { SLACK_PREAMBLE } from './slack/preamble'
|
||||
|
||||
const IN_PROGRESS_MESSAGE = '...✍️'
|
||||
|
||||
/**
|
||||
* Used to test Slack channel context fetching.
|
||||
* E.g., @cody-dev channel:ask-cody your prompt.
|
||||
*/
|
||||
function parseSlackChannelFilter(input: string): string | null {
|
||||
const match = input.match(/channel:([\w-]+)/)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles human-generated messages in a Slack bot application.
|
||||
* Processes the messages, generates a prompt, and streams completions.
|
||||
*/
|
||||
export async function handleHumanMessage(event: AppMentionEvent, codebaseContext: CodebaseContext): Promise<void> {
|
||||
export async function handleHumanMessage(event: AppMentionEvent, appContext: AppContext): Promise<void> {
|
||||
const channel = event.channel
|
||||
const thread_ts = slackHelpers.getEventTs(event)
|
||||
const slackChannelFilter = parseSlackChannelFilter(event.text)
|
||||
|
||||
// Restore transcript from the Slack thread
|
||||
const messages = await slackHelpers.getThreadMessages(channel, thread_ts)
|
||||
const transcript = await restoreTranscriptFromSlackThread(codebaseContext, messages)
|
||||
const [messages, channelName] = await Promise.all([
|
||||
slackHelpers.getThreadMessages(channel, thread_ts),
|
||||
slackHelpers.getSlackChannelName(channel),
|
||||
])
|
||||
|
||||
const transcript = await restoreTranscriptFromSlackThread(slackChannelFilter || channelName!, appContext, messages)
|
||||
|
||||
// Send an in-progress message
|
||||
const response = await slackHelpers.postMessage(IN_PROGRESS_MESSAGE, channel, thread_ts)
|
||||
@ -33,31 +46,53 @@ export async function handleHumanMessage(event: AppMentionEvent, codebaseContext
|
||||
// Generate a prompt and start completion streaming
|
||||
const prompt = await transcript.toPrompt(SLACK_PREAMBLE)
|
||||
console.log('PROMPT', prompt)
|
||||
startCompletionStreaming(prompt, channel, response?.ts)
|
||||
startCompletionStreaming(prompt, channel, transcript, response?.ts)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a transcript from the given Slack thread messages.
|
||||
*/
|
||||
async function restoreTranscriptFromSlackThread(codebaseContext: CodebaseContext, messages: SlackReplyMessage[]) {
|
||||
async function restoreTranscriptFromSlackThread(
|
||||
channelName: string,
|
||||
appContext: AppContext,
|
||||
messages: SlackReplyMessage[]
|
||||
) {
|
||||
const { codebaseContexts, vectorStore } = appContext
|
||||
const transcript = new Transcript()
|
||||
|
||||
const mergedMessages = mergeSequentialUserMessages(messages)
|
||||
const newHumanMessage = mergedMessages.pop()!
|
||||
|
||||
for (const [index, message] of mergedMessages.entries()) {
|
||||
const interaction = await interactionFromMessage(
|
||||
message.human,
|
||||
intentDetector,
|
||||
// Fetch codebase context only for the last message
|
||||
index === mergedMessages.length - 1 ? codebaseContext : null
|
||||
)
|
||||
mergedMessages.forEach(message => {
|
||||
const slackInteraction = getSlackInteraction(message.human.text, message.assistant?.text)
|
||||
|
||||
transcript.addInteraction(interaction)
|
||||
transcript.addInteraction(slackInteraction.getTranscriptInteraction())
|
||||
})
|
||||
|
||||
if (message.assistant?.text) {
|
||||
transcript.addAssistantResponse(message.assistant?.text)
|
||||
}
|
||||
const newHumanSlackInteraction = getSlackInteraction(newHumanMessage?.human.text)
|
||||
|
||||
if (channelName === 'ask-cody') {
|
||||
await Promise.all([
|
||||
newHumanSlackInteraction.updateContextMessagesFromVectorStore(vectorStore, 3),
|
||||
newHumanSlackInteraction.updateContextMessages(codebaseContexts, 'github.com/sourcegraph/sourcegraph', {
|
||||
numCodeResults: 3,
|
||||
numTextResults: 5,
|
||||
}),
|
||||
newHumanSlackInteraction.updateContextMessages(codebaseContexts, 'github.com/sourcegraph/handbook', {
|
||||
numCodeResults: 0,
|
||||
numTextResults: 4,
|
||||
}),
|
||||
])
|
||||
} else {
|
||||
await newHumanSlackInteraction.updateContextMessages(codebaseContexts, 'github.com/sourcegraph/sourcegraph', {
|
||||
numCodeResults: 12,
|
||||
numTextResults: 3,
|
||||
})
|
||||
}
|
||||
|
||||
const lastInteraction = newHumanSlackInteraction.getTranscriptInteraction()
|
||||
transcript.addInteraction(lastInteraction)
|
||||
|
||||
return transcript
|
||||
}
|
||||
|
||||
@ -67,14 +102,30 @@ async function restoreTranscriptFromSlackThread(codebaseContext: CodebaseContext
|
||||
function startCompletionStreaming(
|
||||
promptMessages: PromptMessage[],
|
||||
channel: string,
|
||||
transcript: Transcript,
|
||||
inProgressMessageTs?: string
|
||||
): void {
|
||||
const lastInteraction = transcript.getLastInteraction()!
|
||||
|
||||
const { contextFiles = [] } = lastInteraction.toChat().pop()!
|
||||
|
||||
// Build the markdown list of file links.
|
||||
const contextFilesList = contextFiles
|
||||
.map(file => `[${file.fileName.split('/').pop()}](${file.fileName})`)
|
||||
.join(', ')
|
||||
|
||||
const suffix = contextFiles.length > 0 ? '\n\n**Files used**:\n' + contextFilesList : ''
|
||||
|
||||
streamCompletions(promptMessages, {
|
||||
onChange: text => {
|
||||
onBotMessageChange(reformatBotMessage(text, ''), channel, inProgressMessageTs)?.catch(console.error)
|
||||
// console.log('Stream update: ', text)
|
||||
lastInteraction.setAssistantMessage({ ...lastInteraction.getAssistantMessage(), text })
|
||||
onBotMessageChange(channel, inProgressMessageTs, reformatBotMessage(text, '') + suffix)?.catch(
|
||||
console.error
|
||||
)
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log('Streaming complete!')
|
||||
console.log('Streaming complete!', lastInteraction.getAssistantMessage().text)
|
||||
},
|
||||
onError: err => {
|
||||
console.error(err)
|
||||
@ -86,7 +137,7 @@ function startCompletionStreaming(
|
||||
* Throttled function to update the bot message when there is a change.
|
||||
* Ensures message updates are throttled to avoid exceeding Slack API rate limits.
|
||||
*/
|
||||
const onBotMessageChange = throttle(async (text: string, channel, inProgressMessageTs?: string) => {
|
||||
const onBotMessageChange = throttle(async (channel, inProgressMessageTs: string | undefined, text: string) => {
|
||||
if (inProgressMessageTs) {
|
||||
await slackHelpers.updateMessage(channel, inProgressMessageTs, text)
|
||||
} else {
|
||||
@ -96,8 +147,8 @@ const onBotMessageChange = throttle(async (text: string, channel, inProgressMess
|
||||
}, 1000)
|
||||
|
||||
interface SlackInteraction {
|
||||
human: SlackReplyMessage
|
||||
assistant?: SlackReplyMessage
|
||||
human: { text: string }
|
||||
assistant?: { text: string }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -107,9 +158,10 @@ function mergeSequentialUserMessages(messages: SlackReplyMessage[]) {
|
||||
const mergedMessages: SlackInteraction[] = []
|
||||
|
||||
for (const message of messages) {
|
||||
const text = message.text?.replace(/<@[\dA-Z]+>/gm, '').trim()
|
||||
const lastInteraction = mergedMessages[mergedMessages.length - 1]
|
||||
const updatedMessage = { ...message, blocks: undefined, text }
|
||||
|
||||
const text = cleanupMessageForPrompt(message.text || '', Boolean(message.bot_id))
|
||||
const updatedMessage = { text }
|
||||
|
||||
if (!lastInteraction) {
|
||||
mergedMessages.push({ human: updatedMessage })
|
||||
|
||||
32
client/cody-slack/src/services/github-client.ts
Normal file
32
client/cody-slack/src/services/github-client.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Octokit } from 'octokit'
|
||||
|
||||
import { ENVIRONMENT_CONFIG } from '../constants'
|
||||
|
||||
const octokit = new Octokit({ auth: ENVIRONMENT_CONFIG.GITHUB_TOKEN })
|
||||
|
||||
interface FetchFileContentOptions {
|
||||
owner: string
|
||||
repo: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export async function fetchFileContent(options: FetchFileContentOptions) {
|
||||
const { owner, repo, path } = options
|
||||
|
||||
try {
|
||||
const response = await octokit.rest.repos.getContent({ owner, repo, path })
|
||||
if ('type' in response.data && response.data.type === 'file') {
|
||||
const content = Buffer.from(response.data.content, 'base64').toString('utf8')
|
||||
return {
|
||||
content,
|
||||
url: response.data.html_url,
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Unexpected response fetching file from GitHub:', response)
|
||||
} catch (error) {
|
||||
console.error('Error fetching file from GitHub!', error)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
61
client/cody-slack/src/services/local-vector-store.ts
Normal file
61
client/cody-slack/src/services/local-vector-store.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
|
||||
import { MarkdownTextSplitter } from 'langchain/text_splitter'
|
||||
import { HNSWLib } from 'langchain/vectorstores/hnswlib'
|
||||
|
||||
import { fetchFileContent } from './github-client'
|
||||
|
||||
async function getDocuments() {
|
||||
const codyNotice = await fetchFileContent({
|
||||
owner: 'sourcegraph',
|
||||
repo: 'about',
|
||||
path: 'content/terms/cody-notice.md',
|
||||
})
|
||||
|
||||
if (!codyNotice) {
|
||||
return []
|
||||
}
|
||||
|
||||
const { content, url } = codyNotice
|
||||
const splitter = new MarkdownTextSplitter()
|
||||
const documents = await splitter.createDocuments([content])
|
||||
|
||||
documents.map((document, index) => {
|
||||
document.metadata = {
|
||||
fileName: url,
|
||||
hnswLabel: index,
|
||||
}
|
||||
|
||||
return document
|
||||
})
|
||||
|
||||
return documents
|
||||
}
|
||||
|
||||
const VECTOR_UPDATE_TIMEOUT = 12 * 60 * 60 * 1000
|
||||
|
||||
function scheduleVectorUpdate(vectorStore: HNSWLib, timeout: number) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
vectorStore._index = undefined
|
||||
vectorStore.docstore._docs.clear()
|
||||
|
||||
const documents = await getDocuments()
|
||||
await vectorStore.addDocuments(documents)
|
||||
} catch (error) {
|
||||
console.error('Failed to update vectors', error)
|
||||
} finally {
|
||||
scheduleVectorUpdate(vectorStore, timeout)
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
export async function getVectorStore() {
|
||||
const documents = await getDocuments()
|
||||
|
||||
const embeddings = new OpenAIEmbeddings()
|
||||
const vectorStore = await HNSWLib.fromDocuments(documents, embeddings)
|
||||
|
||||
scheduleVectorUpdate(vectorStore, VECTOR_UPDATE_TIMEOUT)
|
||||
|
||||
return vectorStore
|
||||
}
|
||||
@ -12,10 +12,11 @@ import {
|
||||
export class OpenAICompletionsClient implements Pick<SourcegraphCompletionsClient, 'stream'> {
|
||||
private openai: OpenAIApi
|
||||
|
||||
constructor(private apiKey: string) {
|
||||
constructor(protected apiKey: string) {
|
||||
const configuration = new Configuration({
|
||||
apiKey: this.apiKey,
|
||||
})
|
||||
|
||||
this.openai = new OpenAIApi(configuration)
|
||||
}
|
||||
|
||||
@ -23,7 +24,8 @@ export class OpenAICompletionsClient implements Pick<SourcegraphCompletionsClien
|
||||
this.openai
|
||||
.createChatCompletion(
|
||||
{
|
||||
model: 'gpt-3.5-turbo',
|
||||
// TODO: manage prompt length
|
||||
model: 'gpt-4',
|
||||
messages: params.messages
|
||||
.filter(
|
||||
(message): message is Omit<Message, 'text'> & Required<Pick<Message, 'text'>> =>
|
||||
@ -43,10 +45,12 @@ export class OpenAICompletionsClient implements Pick<SourcegraphCompletionsClien
|
||||
const stream = response.data as unknown as IncomingMessage
|
||||
|
||||
let modelResponseText = ''
|
||||
let buffer = ''
|
||||
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
// Split messages in the event stream.
|
||||
const payloads = chunk.toString().split('\n\n')
|
||||
buffer += chunk.toString()
|
||||
const payloads = buffer.split('\n\n')
|
||||
|
||||
for (const payload of payloads) {
|
||||
if (payload.includes('[DONE]')) {
|
||||
@ -64,17 +68,27 @@ export class OpenAICompletionsClient implements Pick<SourcegraphCompletionsClien
|
||||
modelResponseText += newTextChunk
|
||||
cb.onChange(modelResponseText)
|
||||
}
|
||||
|
||||
buffer = buffer.slice(Math.max(0, buffer.indexOf(payload) + payload.length))
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Error with JSON.parse: ${chunk.toString()}\nPayload: ${payload};\nError: ${error}`
|
||||
)
|
||||
cb.onError(error)
|
||||
if (error instanceof SyntaxError && buffer.length > 0) {
|
||||
// Incomplete JSON string, wait for more data
|
||||
continue
|
||||
} else {
|
||||
console.log(
|
||||
`Error with JSON.parse: ${chunk.toString()}\nPayload: ${payload};\nError: ${error}`
|
||||
)
|
||||
cb.onError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
stream.on('error', e => cb.onError(e.message))
|
||||
stream.on('error', e => {
|
||||
console.error('OpenAI stream failed', e)
|
||||
cb.onError(e.message)
|
||||
})
|
||||
stream.on('end', () => cb.onComplete())
|
||||
})
|
||||
.catch(console.error)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { KnownEventFromType, SlackEvent } from '@slack/bolt'
|
||||
import { ChatPostMessageResponse } from '@slack/web-api'
|
||||
import slackifyMarkdown from 'slackify-markdown'
|
||||
|
||||
import { webClient } from './init'
|
||||
|
||||
@ -12,6 +13,14 @@ export async function getThreadMessages(channel: string, thread_ts: string) {
|
||||
return result?.messages || []
|
||||
}
|
||||
|
||||
export async function getSlackChannelName(channel: string) {
|
||||
const result = await webClient.conversations.info({
|
||||
channel,
|
||||
})
|
||||
|
||||
return result.channel?.name
|
||||
}
|
||||
|
||||
export function getEventTs(event: KnownEventFromType<'app_mention'> | KnownEventFromType<'message'>) {
|
||||
if ('thread_ts' in event && event.thread_ts !== event.ts) {
|
||||
return event.thread_ts || event.ts
|
||||
@ -28,7 +37,35 @@ export async function updateMessage(channel: string, messageTs: string, newText:
|
||||
const response = await webClient.chat.update({
|
||||
channel,
|
||||
ts: messageTs, // The timestamp of the message you want to update.
|
||||
text: newText, // The new text for the updated message.
|
||||
text: slackifyMarkdown(newText), // The new text for the updated message.
|
||||
mrkdwn: true,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error updating message: ${response.error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMessageWithFileList(
|
||||
channel: string,
|
||||
messageTs: string,
|
||||
newText: string,
|
||||
fileList: string
|
||||
): Promise<void> {
|
||||
const response = await webClient.chat.update({
|
||||
channel,
|
||||
blocks: [
|
||||
{
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: slackifyMarkdown(fileList),
|
||||
},
|
||||
},
|
||||
],
|
||||
ts: messageTs, // The timestamp of the message you want to update.
|
||||
text: slackifyMarkdown(newText), // The new text for the updated message.
|
||||
mrkdwn: true,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@ -43,8 +80,9 @@ export async function postMessage(
|
||||
): Promise<ChatPostMessageResponse | undefined> {
|
||||
const response = await webClient.chat.postMessage({
|
||||
channel,
|
||||
text: message,
|
||||
text: slackifyMarkdown(message),
|
||||
thread_ts, // Use the timestamp of the parent message to reply in the thread.
|
||||
mrkdwn: true,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@ -1,53 +1,93 @@
|
||||
import { Message } from '@slack/web-api/dist/response/ChannelsRepliesResponse'
|
||||
import { HNSWLib } from 'langchain/vectorstores/hnswlib'
|
||||
|
||||
import { Interaction } from '@sourcegraph/cody-shared/src/chat/transcript/interaction'
|
||||
import { CodebaseContext } from '@sourcegraph/cody-shared/src/codebase-context'
|
||||
import { ContextMessage } from '@sourcegraph/cody-shared/src/codebase-context/messages'
|
||||
import { IntentDetector } from '@sourcegraph/cody-shared/src/intent-detector'
|
||||
import { InteractionMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
|
||||
import { ContextSearchOptions } from '@sourcegraph/cody-shared/src/codebase-context'
|
||||
import { ContextMessage, getContextMessageWithResponse } from '@sourcegraph/cody-shared/src/codebase-context/messages'
|
||||
// import { IntentDetector } from '@sourcegraph/cody-shared/src/intent-detector'
|
||||
import { MAX_HUMAN_INPUT_TOKENS } from '@sourcegraph/cody-shared/src/prompt/constants'
|
||||
import { populateMarkdownContextTemplate } from '@sourcegraph/cody-shared/src/prompt/templates'
|
||||
import { truncateText } from '@sourcegraph/cody-shared/src/prompt/truncation'
|
||||
|
||||
export async function interactionFromMessage(
|
||||
message: Message,
|
||||
intentDetector: IntentDetector,
|
||||
codebaseContext: CodebaseContext | null
|
||||
): Promise<Interaction | null> {
|
||||
if (!message.text) {
|
||||
return Promise.resolve(null)
|
||||
import { CodebaseContexts } from '../constants'
|
||||
|
||||
class SlackInteraction {
|
||||
public contextMessages: ContextMessage[] = []
|
||||
|
||||
constructor(private humanMessage: InteractionMessage, private assistantMessage: InteractionMessage) {}
|
||||
|
||||
public async updateContextMessagesFromVectorStore(vectorStore: HNSWLib, numResults: number) {
|
||||
const docs = await vectorStore.similaritySearch(this.humanMessage.text!, numResults)
|
||||
|
||||
docs.forEach(doc => {
|
||||
const contextMessage = getContextMessageWithResponse(
|
||||
populateMarkdownContextTemplate(doc.pageContent, doc.metadata.fileName),
|
||||
doc.metadata.fileName
|
||||
)
|
||||
this.contextMessages.push(...contextMessage)
|
||||
})
|
||||
}
|
||||
|
||||
const textWithoutMentions = message.text?.replace(/<@[\dA-Z]+>/gm, '').trim()
|
||||
const text = truncateText(textWithoutMentions, MAX_HUMAN_INPUT_TOKENS)
|
||||
public async updateContextMessages(
|
||||
codebaseContexts: CodebaseContexts,
|
||||
codebase: keyof CodebaseContexts,
|
||||
contextSearchOptions: ContextSearchOptions
|
||||
// intentDetector?: IntentDetector
|
||||
) {
|
||||
// const isCodebaseContextRequired = await intentDetector.isCodebaseContextRequired(text)
|
||||
const isCodebaseContextRequired = true
|
||||
|
||||
const contextMessages =
|
||||
codebaseContext === null ? Promise.resolve([]) : getContextMessages(text, intentDetector, codebaseContext)
|
||||
if (isCodebaseContextRequired) {
|
||||
const contextMessages = await codebaseContexts[codebase].getContextMessages(
|
||||
this.humanMessage.text!,
|
||||
contextSearchOptions
|
||||
)
|
||||
|
||||
return Promise.resolve(
|
||||
new Interaction(
|
||||
{ speaker: 'human', text, displayText: text },
|
||||
{ speaker: 'assistant', text: '', displayText: '' },
|
||||
contextMessages
|
||||
)
|
||||
this.contextMessages.push(
|
||||
...contextMessages.map(message => {
|
||||
if (message.file) {
|
||||
message.file.fileName = `https://${codebase}/blob/main/${message.file.fileName}`
|
||||
}
|
||||
|
||||
return message
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public getTranscriptInteraction() {
|
||||
return new Interaction(this.humanMessage, this.assistantMessage, Promise.resolve(this.contextMessages))
|
||||
}
|
||||
}
|
||||
|
||||
export function getSlackInteraction(humanText: string, assistantText: string = ''): SlackInteraction {
|
||||
const text = cleanupMessageForPrompt(humanText)
|
||||
const filteredHumanText = truncateText(text, MAX_HUMAN_INPUT_TOKENS)
|
||||
|
||||
return new SlackInteraction(
|
||||
{ speaker: 'human', text: filteredHumanText },
|
||||
{ speaker: 'assistant', text: assistantText }
|
||||
)
|
||||
}
|
||||
|
||||
export async function getContextMessages(
|
||||
text: string,
|
||||
intentDetector: IntentDetector,
|
||||
codebaseContext: CodebaseContext
|
||||
): Promise<ContextMessage[]> {
|
||||
const contextMessages: ContextMessage[] = []
|
||||
export function cleanupMessageForPrompt(text: string, isAssistantMessage = false) {
|
||||
// Delete mentions
|
||||
const textWithoutMentions = text.replace(/<@[\dA-Z]+>/gm, '').trim()
|
||||
|
||||
const isCodebaseContextRequired = await intentDetector.isCodebaseContextRequired(text)
|
||||
// Delete cody-slack filters
|
||||
const textWithoutFilters = textWithoutMentions.replace(/channel:([\w-]+)/gm, '').trim()
|
||||
|
||||
if (isCodebaseContextRequired) {
|
||||
const codebaseContextMessages = await codebaseContext.getContextMessages(text, {
|
||||
numCodeResults: 8,
|
||||
numTextResults: 2,
|
||||
})
|
||||
if (isAssistantMessage) {
|
||||
// Delete "Files used" section
|
||||
const filesSectionIndex = textWithoutFilters.lastIndexOf('*Files used*')
|
||||
|
||||
contextMessages.push(...codebaseContextMessages)
|
||||
if (filesSectionIndex !== -1) {
|
||||
return textWithoutFilters
|
||||
.slice(0, filesSectionIndex)
|
||||
.replace(/[|\u00A0\u200B\u200D]/gm, '')
|
||||
.replace(/\n+$/gm, '')
|
||||
}
|
||||
}
|
||||
|
||||
return contextMessages
|
||||
return textWithoutFilters
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { Message } from '@sourcegraph/cody-shared/src/sourcegraph-api'
|
||||
|
||||
import { DEFAULT_APP_SETTINGS } from '../constants'
|
||||
|
||||
const actions = `You are Cody, an AI-powered coding assistant created by Sourcegraph. You work inside a Slack workspace. You have access to the Slack thread conversation with all the replies. You perform the following actions:
|
||||
- Answer general programming questions.
|
||||
- Answer general questions about the Slack thread you're in.
|
||||
@ -10,14 +8,14 @@ const actions = `You are Cody, an AI-powered coding assistant created by Sourceg
|
||||
- Explain what a section of code does.`
|
||||
|
||||
const rules = `In your responses, obey the following rules:
|
||||
- Be as brief and concise as possible without losing clarity.
|
||||
- The current Slack thread is the same as the conversation you're having with the user. Use this information to answer questions.
|
||||
- Be brief without losing clarity.
|
||||
- Use GitHub markdown to format your messages in the most readable way for humans. Use markdown lists.
|
||||
- All code snippets have to be markdown-formatted without that language specifier, and placed in-between triple backticks like this \`\`\`.
|
||||
- Answer questions only if you know the answer or can make a well-informed guess. Otherwise, tell me you don't know and what context I need to provide you for you to answer the question.
|
||||
- Only reference file names or URLs if you are sure they exist.`
|
||||
|
||||
const answer = `Understood. I am Cody, an AI assistant made by Sourcegraph to help with programming tasks and assist in Slack conversations.
|
||||
I work inside a Slack workspace. I have access have access to the currently active Slack thread conversation with all the replies.
|
||||
I use GitHub markdwon to format my responses in the most readable way for humans.
|
||||
I will answer questions, explain code, and generate code as concisely and clearly as possible.
|
||||
My responses will be formatted using Markdown syntax for code blocks without language specifiers.
|
||||
I will acknowledge when I don't know an answer or need more context. I will use the Slack thread conversation history to answer your questions.`
|
||||
@ -26,21 +24,10 @@ I will acknowledge when I don't know an answer or need more context. I will use
|
||||
* Creates and returns an array of two messages: one from a human, and the supposed response from the AI assistant.
|
||||
* Both messages contain an optional note about the current codebase if it's not null.
|
||||
*/
|
||||
function getSlackPreamble(codebase: string): Message[] {
|
||||
function getSlackPreamble(): Message[] {
|
||||
const preamble = [actions, rules]
|
||||
const preambleResponse = [answer]
|
||||
|
||||
if (codebase) {
|
||||
const codebasePreamble =
|
||||
`You have access to the \`${codebase}\` repository. You are able to answer questions about the \`${codebase}\` repository. ` +
|
||||
`I will provide the relevant code snippets from the \`${codebase}\` repository when necessary to answer my questions.`
|
||||
|
||||
preamble.push(codebasePreamble)
|
||||
preambleResponse.push(
|
||||
`I have access to the \`${codebase}\` repository and can answer questions about its files.`
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
speaker: 'human',
|
||||
@ -53,4 +40,4 @@ function getSlackPreamble(codebase: string): Message[] {
|
||||
]
|
||||
}
|
||||
|
||||
export const SLACK_PREAMBLE = getSlackPreamble(DEFAULT_APP_SETTINGS.codebase)
|
||||
export const SLACK_PREAMBLE = getSlackPreamble()
|
||||
|
||||
@ -501,6 +501,11 @@
|
||||
"packageManager": "pnpm@8.1.0",
|
||||
"pnpm": {
|
||||
"packageExtensions": {
|
||||
"hnswlib-node": {
|
||||
"dependencies": {
|
||||
"node-gyp": "*"
|
||||
}
|
||||
},
|
||||
"cpu-features": {
|
||||
"dependencies": {
|
||||
"node-gyp": "*"
|
||||
|
||||
750
pnpm-lock.yaml
750
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user