diff --git a/provider/zendesk/api.ts b/provider/zendesk/api.ts index 217127e7..c87e8418 100644 --- a/provider/zendesk/api.ts +++ b/provider/zendesk/api.ts @@ -1,122 +1,216 @@ import type { Settings } from './index.ts' -export interface TicketPickerItem { +export interface SearchResults { + subject: string + tickets: Ticket[] +} + +export interface SearchResponse { + results: SearchResult[] +} + +interface SearchResult { id: number subject: string - url: string + created_at: string } export interface Ticket { id: number - url: string subject: string - description: string - tags: string[] - status: string - priority: string created_at: string - updated_at: string comments: TicketComment[] + summary?: string + summaries: Summary[] +} + +export interface Summary { + id: number + summary?: string + subject: string } export interface TicketComment { id: number - type: string author_id: number - body: string - html_body: string plain_body: string - public: boolean created_at: string } +export interface ChatCompletionRequest { + messages: Array<{ role: string; content: string }> + model: string + max_tokens: number + stream: boolean +} + +export interface ChatCompletionResponse { + message: string + choices: Array<{ + message: { + content: string + } + }> +} + const authHeaders = (settings: Settings) => ({ Authorization: `Basic ${Buffer.from(`${settings.email}/token:${settings.apiToken}`).toString('base64')}`, }) +const sgTokenHeaders = (settings: Settings) => ({ + Authorization: `token ${settings.sgToken}`, +}) + const buildUrl = (settings: Settings, path: string, searchParams: Record = {}) => { const url = new URL(`https://${settings.subdomain}.zendesk.com/api/v2${path}`) url.search = new URLSearchParams(searchParams).toString() return url } +/** + * Searches Zendesk tickets based on a query string and returns matching tickets with their details. + * @param query - The search query string to filter tickets + * @param settings - Zendesk API settings including subdomain, email, and API token + * @returns Promise resolving to SearchResults containing matched tickets and their subjects + */ + export const searchTickets = async ( query: string | undefined, settings: Settings, -): Promise => { +): Promise => { const searchResponse = await fetch( buildUrl(settings, '/search.json', { - query: `type:ticket ${query || ''}`, + // Add search parameters here + query: `${query} order_by:created sort:desc` || '', }), { method: 'GET', headers: authHeaders(settings), - }, + } ) if (!searchResponse.ok) { throw new Error( - `Error searching Zendesk tickets (${searchResponse.status} ${ - searchResponse.statusText + `Error searching Zendesk tickets (${searchResponse.status} ${searchResponse.statusText }): ${await searchResponse.text()}`, ) } - const searchJSON = (await searchResponse.json()) as { - results: { - id: number - subject: string - url: string - }[] + const result = (await searchResponse.json()) as SearchResponse + + const searchResults: SearchResults = { + subject: '', + tickets: [] } - return searchJSON.results.map(ticket => ({ - id: ticket.id, - subject: ticket.subject, - url: ticket.url, - })) + for (const item of result.results) { + searchResults.subject += `${item.id.toString()} ${item.subject}, ` + searchResults.tickets.push({ + id: item.id, + subject: item.subject, + created_at: item.created_at, + comments: [], + summaries: [] + }) + // This is to limit the number of tickets. + if (searchResults.tickets.length === 5) { + break + } + } + return searchResults } -export const fetchTicket = async (ticketId: number, settings: Settings): Promise => { - const ticketResponse = await fetch( - buildUrl(settings, `/tickets/${ticketId}.json`), +/** + * Fetches comments for a given Zendesk ticket + * @param ticket - The ticket object to fetch comments for + * @param settings - Zendesk API settings including subdomain, email, and API token + * @returns Promise resolving to Ticket object with comments field populated + */ +export const fetchComments = async (ticket: Ticket, settings: Settings): Promise => { + const commentsResponse = await fetch( + buildUrl(settings, `/tickets/${ticket.id}/comments.json`), { method: 'GET', headers: authHeaders(settings), } ) - if (!ticketResponse.ok) { + if (!commentsResponse.ok) { throw new Error( - `Error fetching Zendesk ticket (${ticketResponse.status} ${ - ticketResponse.statusText - }): ${await ticketResponse.text()}` + `Error fetching Zendesk ticket comments (${commentsResponse.status} ${commentsResponse.statusText + }): ${await commentsResponse.text()}` ) } - const responseJSON = (await ticketResponse.json()) as { ticket: Ticket } - const ticket = responseJSON.ticket + const commentsJSON = (await commentsResponse.json() as { comments: TicketComment[] }).comments + // Extract only necessary fields from comments + const comments: TicketComment[] = commentsJSON.map(comment => ({ + id: comment.id, + author_id: comment.author_id, + plain_body: comment.plain_body, + created_at: comment.created_at, + })) + + return { ...ticket, comments } +} + +/** + * Fetches a chat completion from the Sourcegraph API to generate a summary for a Zendesk ticket + * @param settings - Sourcegraph API settings + * @param ticket - The ticket object to generate a summary for + * @returns Promise resolving to Ticket object with summary field populated + */ +export const fetchChatCompletion = async ( + settings: Settings, + ticket: Ticket +): Promise => { - if (!ticket) { - return null + const formatComments = (comments: TicketComment[]): string => { + return comments.map(comment => { + return `Comment ID: ${comment.id}\nAuthor ID: ${comment.author_id}\nContent: ${comment.plain_body}\nCreated At: ${comment.created_at}\n` + }).join('\n') } - // Fetch comments for the ticket - const commentsResponse = await fetch( - buildUrl(settings, `/tickets/${ticketId}/comments.json`), - { - method: 'GET', - headers: authHeaders(settings), - } - ) - if (!commentsResponse.ok) { - throw new Error( - `Error fetching Zendesk ticket comments (${commentsResponse.status} ${ - commentsResponse.statusText - }): ${await commentsResponse.text()}` - ) + const ticketContent = ` + Ticket ID: ${ticket.id} + Subject: ${ticket.subject || 'N/A'} + Created At: ${ticket.created_at || 'N/A'} + Comments: ${formatComments(ticket.comments)} + ` + const requestData: ChatCompletionRequest = { + messages: [ + { + role: 'user', + content: `${settings.prompt} ${ticketContent}` + } + ], + model: `${settings.model}`, + max_tokens: 1000, + stream: false } - const commentsJSON = (await commentsResponse.json()) as { comments: TicketComment[] } - ticket.comments = commentsJSON.comments + const response = await fetch(`${settings.sgDomain}`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Requested-With': 'cody-api v1', + ...sgTokenHeaders(settings), + }, + body: JSON.stringify(requestData), + }) + + if (!response.ok) { + throw new Error(`Error fetching chat completion (${response.status} ${response.statusText}): ${await response.text()}`) + } - return ticket + const summary = (await response.json() as ChatCompletionResponse).choices[0].message.content + + return { ...ticket, summary } } + + +export const fetchSummary = async (ticket: Ticket, settings: Settings): Promise => { + const ticketWithComments = await fetchComments(ticket, settings) + const ticketWithSummary = await fetchChatCompletion(settings, ticketWithComments) + // Return the ticket with the summary + return ticketWithSummary +} \ No newline at end of file diff --git a/provider/zendesk/index.ts b/provider/zendesk/index.ts index e7486c29..6abb9a2e 100644 --- a/provider/zendesk/index.ts +++ b/provider/zendesk/index.ts @@ -1,5 +1,4 @@ import type { - Item, ItemsParams, ItemsResult, MentionsParams, @@ -8,88 +7,89 @@ import type { MetaResult, Provider, } from '@openctx/provider' -import { type Ticket, fetchTicket, searchTickets } from './api.js' +import { type Summary, Ticket, fetchSummary, searchTickets } from './api.js' export type Settings = { subdomain: string email: string apiToken: string + sgToken: string + sgDomain: string + prompt: string + model: string } const checkSettings = (settings: Settings) => { - const missingKeys = ['subdomain', 'email', 'apiToken'].filter(key => !(key in settings)) + const missingKeys = ['subdomain', 'email', 'apiToken', 'sgToken', 'sgDomain', 'prompt', 'model'].filter(key => !(key in settings)) if (missingKeys.length > 0) { throw new Error(`Missing settings: ${JSON.stringify(missingKeys)}`) } } -const ticketToItem = (ticket: Ticket): Item => ({ - url: ticket.url, - title: ticket.subject, - ui: { - hover: { - markdown: ticket.description, - text: ticket.description || ticket.subject, - }, - }, - ai: { - content: - `The following represents contents of the Zendesk ticket ${ticket.id}: ` + - JSON.stringify({ - ticket: { - id: ticket.id, - subject: ticket.subject, - url: ticket.url, - description: ticket.description, - tags: ticket.tags, - status: ticket.status, - priority: ticket.priority, - created_at: ticket.created_at, - updated_at: ticket.updated_at, - comments: ticket.comments.map(comment => ({ - id: comment.id, - type: comment.type, - author_id: comment.author_id, - body: comment.body, - html_body: comment.html_body, - plain_body: comment.plain_body, - public: comment.public, - created_at: comment.created_at, - })) - }, - }), - }, -}) - const zendeskProvider: Provider = { meta(params: MetaParams, settings: Settings): MetaResult { return { name: 'Zendesk', mentions: { label: 'Search by subject, id, or paste url...' } } }, async mentions(params: MentionsParams, settings: Settings): Promise { + if (!params.query) { + return [] + } + checkSettings(settings) - return searchTickets(params.query, settings).then(items => - items.map(item => ({ - title: `#${item.id}`, - uri: item.url, - description: item.subject, - data: { id: item.id }, - })), - ) + const ticketResults = await searchTickets(params.query, settings) + + return [ + { + title: ticketResults.subject, + uri: '', + description: ticketResults.subject, + data: { tickets: ticketResults.tickets }, + }, + ] }, async items(params: ItemsParams, settings: Settings): Promise { checkSettings(settings) - const id = (params.mention?.data as { id: number }).id + const tickets = (params.mention?.data as { tickets: Ticket[] }).tickets + const allSummaries: Summary[] = [] + + const fetchSummaries = await Promise.all( + tickets.map(ticket => fetchSummary(ticket, settings)) + ) - const ticket = await fetchTicket(id, settings) + fetchSummaries.forEach(summary => { + if (summary) { + allSummaries.push({ id: summary.id, summary: summary.summary, subject: summary.subject }) + } + }) - if (!ticket) { + if (!allSummaries) { return [] } - return [ticketToItem(ticket)] + console.log('allSummaries', allSummaries) + + return [{ + url: '', + title: params.mention?.title || '', + ui: { + hover: { + markdown: '', + text: '', + }, + }, + ai: { + content: + `The following represents contents of the Zendesk tickets:` + + JSON.stringify({ + ticket: { + summaries: allSummaries + }, + }), + }, + }] }, }