Skip to content

Commit

Permalink
Merge pull request #28 from tidbcloud/feat/use-chat-api
Browse files Browse the repository at this point in the history
feat(chat-api): use chat api
  • Loading branch information
baurine authored Jun 27, 2024
2 parents 7366a4e + c5f05b4 commit 42c073f
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 13 deletions.
6 changes: 3 additions & 3 deletions packages/extensions/ai-widget/src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { EditorView } from '@codemirror/view'
export type ChatReq = {
prompt: string
refContent: string
extra?: {}
extra?: any
}

export type ChatRes = {
status: 'success' | 'error'
message: string
extra?: {}
extra?: any
}

type EventType =
export type EventType =
| 'widget.open' // {source: 'hotkey' | 'placeholder' | 'fix_sql_button' | ...}
| 'no_use_db.error'
| 'req.send' // {chatReq}
Expand Down
1 change: 1 addition & 0 deletions packages/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@tanstack/react-query": "^5.45.1",
"@tidbcloud/codemirror-extension-ai-widget": "workspace:^",
"@tidbcloud/codemirror-extension-autocomplete": "workspace:^",
"@tidbcloud/codemirror-extension-cur-sql": "workspace:^",
"@tidbcloud/codemirror-extension-cur-sql-gutter": "workspace:^",
"@tidbcloud/codemirror-extension-linters": "workspace:^",
"@tidbcloud/codemirror-extension-save-helper": "workspace:^",
Expand Down
5 changes: 4 additions & 1 deletion packages/playground/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { FilesProvider } from '@/contexts/files-context-provider'
import { SchemaProvider } from '@/contexts/schema-context-provider'

import { EditorExample } from '@/examples/editor-example'
import { ChatProvider } from './contexts/chat-context-provider'

const queryClient = new QueryClient()

Expand All @@ -20,7 +21,9 @@ function Full() {
<StatementProvider>
<SchemaProvider>
<FilesProvider>
<Panels />
<ChatProvider>
<Panels />
</ChatProvider>
</FilesProvider>
</SchemaProvider>
</StatementProvider>
Expand Down
182 changes: 182 additions & 0 deletions packages/playground/src/api/tidbcloud/chat-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { delay } from '@/lib/delay'
import { ChatRes } from '@tidbcloud/codemirror-extension-ai-widget'

//---------------------------------
// a simple way to cancel loop query

const queryingJobs: Map<string, boolean> = new Map()

export function cancelChat(chatId: string) {
queryingJobs.delete(chatId)
}

//---------------------------------

async function queryJobStatus(jobId: string) {
// res example:
// ---------
// for refine sql job:
// {
// "code": 200,
// "msg": "",
// "result": {
// "ended_at": 1719471325,
// "job_id": "f80099a82d20475d8da24b20dc817e67",
// "reason": "",
// "result": {
// "rewritten_sql": "SELECT * FROM `games` ORDER BY `recommendations` DESC LIMIT 10;",
// "solution": "To address the user feedback of limiting the results to 10 instead of 20, we can simply update the LIMIT clause in the SQL query."
// },
// "status": "done"
// }
// }
// ---------
// for chat2data job:
// {
// "code": 200,
// "msg": "",
// "result": {
// "ended_at": 1719461464,
// "job_id": "f6f46a745a264a50bf0e60163b670b9d",
// "reason": "",
// "result": {
// "sql": "SELECT * FROM `games` ORDER BY `recommendations` DESC LIMIT 20;",
// "sql_error": null
// // ...
// },
// "status": "done"
// }
// }
return fetch(`/api/jobs?job_id=${jobId}`)
.then((res) => {
if (res.status >= 400 || res.status < 200) {
return res.json().then((d) => {
throw new Error(d.msg)
})
}
return res
})
.then((res) => res.json())
.then((d) => d.result)
}

async function loopQueryJob(chatId: string, jobId: string): Promise<ChatRes> {
// only try 5 times to reduce rate limit (current 100 times a day)
let i = 5
while (i > 0) {
i--

// check whether job is canceled
if (!queryingJobs.get(chatId)) {
return { status: 'error', message: 'chat is canceled', extra: {} }
}

const jobRes = await queryJobStatus(jobId)
if (jobRes.status === 'done') {
return {
status: 'success',
message: jobRes.result.rewritten_sql ?? jobRes.result.sql ?? '',
extra: {}
}
} else if (jobRes.status === 'failed') {
return { status: 'error', message: jobRes.reason, extra: {} }
}
await delay(10 * 1000)
}
throw new Error('Request timed out. Please try again.')
}

//-----------------

type Chat2DataReq = {
database: string
question: string
}

export async function chat2data(
chatId: string,
params: Chat2DataReq
): Promise<ChatRes> {
queryingJobs.set(chatId, true)

try {
// res example:
// {
// "code": 200,
// "msg": "",
// "result": {
// "cluster_id": "xxx",
// "database": "game",
// "job_id": "yyy",
// "session_id": zzz
// }
// }
const res = await fetch(`/api/chat2data`, {
method: 'POST',
body: JSON.stringify(params)
})
.then((res) => {
if (res.status >= 400 || res.status < 200) {
return res.json().then((d) => {
throw new Error(d.msg)
})
}
return res
})
.then((res) => res.json())
.then((d) => d.result)

const jobId = res.job_id
const jobRes = await loopQueryJob(chatId, jobId)

return jobRes
} catch (error: any) {
return { status: 'error', message: error.message, extra: {} }
}
}

type RefineSqlReq = {
database: string
sql: string
feedback: string
}

export async function refineSql(
chatId: string,
params: RefineSqlReq
): Promise<ChatRes> {
queryingJobs.set(chatId, true)

try {
// res example:
// {
// "code": 200,
// "msg": "",
// "result": {
// "job_id": "xxx",
// "session_id": "yyy"
// }
// }
const res = await fetch(`/api/refineSql`, {
method: 'POST',
body: JSON.stringify(params)
})
.then((res) => {
if (res.status >= 400 || res.status < 200) {
return res.json().then((d) => {
throw new Error(d.msg)
})
}
return res
})
.then((res) => res.json())
.then((d) => d.result)

const jobId = res.job_id
const jobRes = await loopQueryJob(chatId, jobId)

return jobRes
} catch (error: any) {
return { status: 'error', message: error.message, extra: {} }
}
}
8 changes: 8 additions & 0 deletions packages/playground/src/api/tidbcloud/schema-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { SchemaRes } from '@/contexts/schema-context'

export async function getSchema(): Promise<SchemaRes> {
return fetch(`/api/schema`)
.then((res) => {
if (res.status >= 400 || res.status < 200) {
return res.json().then((d) => {
throw new Error(d.message)
})
}
return res
})
.then((res) => res.json())
.then((d) => d.data)
}
28 changes: 19 additions & 9 deletions packages/playground/src/components/biz/editor-panel/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo } from 'react'
import { useMemo, useRef } from 'react'

import { EditorView, placeholder } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import { SQLConfig } from '@codemirror/lang-sql'
Expand All @@ -16,11 +17,12 @@ import {
aiWidget,
isUnifiedMergeViewActive
} from '@tidbcloud/codemirror-extension-ai-widget'
import { getCurDatabase } from '@tidbcloud/codemirror-extension-cur-sql'

import { useFilesContext } from '@/contexts/files-context'
import { useTheme } from '@/components/darkmode-toggle/theme-provider'
import { SchemaRes, useSchemaContext } from '@/contexts/schema-context'
import { delay } from '@/lib/delay'
import { useChatContext } from '@/contexts/chat-context'

function convertSchemaToSQLConfig(dbList: SchemaRes): SQLConfig {
const schema: any = {}
Expand Down Expand Up @@ -63,6 +65,12 @@ export function Editor() {
() => convertSchemaToSQLConfig(schema ?? []),
[schema]
)
const getDbListRef = useRef<() => string[]>()
getDbListRef.current = () => {
return schema?.map((d) => d.name) || []
}

const chatCtx = useChatContext()

const activeFile = useMemo(
() => openedFiles.find((f) => f.id === activeFileId),
Expand Down Expand Up @@ -90,14 +98,16 @@ export function Editor() {
}),
fullWidthCharLinter(),
aiWidget({
chat: async () => {
await delay(2000)
return { status: 'success', message: 'select * from test;' }
chat(view, chatId, req) {
const db = getCurDatabase(view.state)
req['extra']['db'] = db
return chatCtx.chat(chatId, { ...req })
},
cancelChat: () => {},
getDbList: () => {
return ['test1', 'test2']
}
cancelChat: chatCtx.cancelChat,
onEvent(_view, type, payload) {
chatCtx.onEvent(type, payload)
},
getDbList: getDbListRef.current!
})
]
}
Expand Down
41 changes: 41 additions & 0 deletions packages/playground/src/contexts/chat-context-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useMemo } from 'react'
import { ChatReq, EventType } from '@tidbcloud/codemirror-extension-ai-widget'

import { cancelChat, chat2data, refineSql } from '@/api/tidbcloud/chat-api'
import { ChatContext } from './chat-context'

function chat(chatId: string, req: ChatReq) {
if (req.refContent === '') {
return chat2data(chatId, {
database: req.extra?.db ?? '',
question: req.prompt
})
}
return refineSql(chatId, {
database: req.extra?.db ?? '',
feedback: req.prompt,
sql: req.refContent
})
}

function onEvent(event: EventType, payload?: {}) {
console.log('event:', event)
console.log('payload:', payload)
}

export function ChatProvider(props: { children: React.ReactNode }) {
const ctxValue = useMemo(
() => ({
chat,
cancelChat,
onEvent
}),
[]
)

return (
<ChatContext.Provider value={ctxValue}>
{props.children}
</ChatContext.Provider>
)
}
25 changes: 25 additions & 0 deletions packages/playground/src/contexts/chat-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createContext, useContext } from 'react'

import {
ChatReq,
ChatRes,
EventType
} from '@tidbcloud/codemirror-extension-ai-widget'

type ChatCtxValue = {
chat: (chatId: string, req: ChatReq) => Promise<ChatRes>
cancelChat: (chatId: string) => void
onEvent: (event: EventType, payload?: {}) => void
}

export const ChatContext = createContext<ChatCtxValue | null>(null)

export const useChatContext = () => {
const context = useContext(ChatContext)

if (!context) {
throw new Error('useChatContext must be used within a provider')
}

return context
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 42c073f

Please sign in to comment.