Skip to main content
Glama
OpenAiCompatibilityService.ts7.76 kB
/** * OpenAI compatibility helpers – expose the universal search + retrieval stack * using the `search`/`fetch` contract that non-Developer-Mode ChatGPT accounts * expect. All logic delegates to the existing universal services so we keep a * single behaviour surface for every client. */ import { UniversalSearchService } from './UniversalSearchService.js'; import { UniversalRetrievalService } from './UniversalRetrievalService.js'; import { UniversalResourceType, UniversalSearchParams, SearchType, MatchType, SortType, } from '../handlers/tool-configs/universal/types.js'; import type { AttioRecord } from '../types/attio.js'; import { SearchUtilities } from './search-utilities/SearchUtilities.js'; import { safeJsonStringify } from '../utils/json-serializer.js'; const DEFAULT_LIMIT = 10; const MAX_LIMIT = 25; const RESOURCE_API_PATH: Partial<Record<UniversalResourceType, string>> = { [UniversalResourceType.COMPANIES]: 'objects/companies/records', [UniversalResourceType.PEOPLE]: 'objects/people/records', [UniversalResourceType.LISTS]: 'lists', [UniversalResourceType.TASKS]: 'tasks', }; const SEARCH_RESOURCE_MAP: Record< 'companies' | 'people' | 'lists' | 'tasks' | 'all', UniversalResourceType[] > = { companies: [UniversalResourceType.COMPANIES], people: [UniversalResourceType.PEOPLE], lists: [UniversalResourceType.LISTS], tasks: [UniversalResourceType.TASKS], all: [ UniversalResourceType.COMPANIES, UniversalResourceType.PEOPLE, UniversalResourceType.LISTS, UniversalResourceType.TASKS, ], }; export interface OpenAiSearchParams { query: string; type?: keyof typeof SEARCH_RESOURCE_MAP; limit?: number; } export interface OpenAiSearchResult { id: string; title: string; url: string; snippet?: string; metadata?: Record<string, unknown>; } export interface OpenAiFetchResult { id: string; title: string; url: string; text: string; metadata?: Record<string, unknown>; } export class OpenAiCompatibilityService { static async search( params: OpenAiSearchParams ): Promise<OpenAiSearchResult[]> { const query = params.query?.trim(); if (!query) { throw new Error('Query must be provided'); } const typeKey = ( params.type ?? 'all' ).toLowerCase() as keyof typeof SEARCH_RESOURCE_MAP; const resourceTypes = SEARCH_RESOURCE_MAP[typeKey] ?? SEARCH_RESOURCE_MAP.all; const requestedLimit = params.limit ?? DEFAULT_LIMIT; const limit = Math.min(Math.max(requestedLimit, 1), MAX_LIMIT); const perTypeLimit = Math.max(1, Math.ceil(limit / resourceTypes.length)); const aggregated: OpenAiSearchResult[] = []; for (const resourceType of resourceTypes) { const searchParams: UniversalSearchParams = { resource_type: resourceType, query, limit: perTypeLimit, search_type: SearchType.BASIC, match_type: MatchType.PARTIAL, sort: SortType.RELEVANCE, }; const records = await UniversalSearchService.searchRecords(searchParams); aggregated.push( ...records.map((record) => transformRecordToSearchResult(resourceType, record) ) ); } return aggregated.slice(0, limit); } static async fetch(id: string): Promise<OpenAiFetchResult> { const { resourceType, recordId } = parseCompoundId(id); const record = await UniversalRetrievalService.getRecordDetails({ resource_type: resourceType, record_id: recordId, }); return transformRecordToFetchResult(resourceType, record); } } function parseCompoundId(id: string): { resourceType: UniversalResourceType; recordId: string; } { if (!id || !id.includes(':')) { throw new Error( 'Expected identifier format "<resource_type>:<record_id>" (e.g. companies:1234)' ); } const [rawType, ...rest] = id.split(':'); const recordId = rest.join(':'); if (!recordId) { throw new Error('Record identifier is missing'); } const resourceType = normalizeResourceType(rawType); return { resourceType, recordId }; } function normalizeResourceType(value: string): UniversalResourceType { switch (value.toLowerCase()) { case 'companies': return UniversalResourceType.COMPANIES; case 'people': return UniversalResourceType.PEOPLE; case 'lists': return UniversalResourceType.LISTS; case 'tasks': return UniversalResourceType.TASKS; default: throw new Error(`Unsupported resource type: ${value}`); } } function transformRecordToSearchResult( resourceType: UniversalResourceType, record: AttioRecord ): OpenAiSearchResult { const recordId = record.id?.record_id ?? 'unknown'; return { id: `${resourceType}:${recordId}`, title: buildTitle(resourceType, record), url: buildApiUrl(resourceType, recordId), snippet: buildSnippet(resourceType, record), metadata: { resource_type: resourceType, }, }; } function transformRecordToFetchResult( resourceType: UniversalResourceType, record: AttioRecord ): OpenAiFetchResult { const recordId = record.id?.record_id ?? 'unknown'; return { id: `${resourceType}:${recordId}`, title: buildTitle(resourceType, record), url: buildApiUrl(resourceType, recordId), text: safeJsonStringify(record, { indent: 2 }), metadata: { resource_type: resourceType, }, }; } function buildTitle( resourceType: UniversalResourceType, record: AttioRecord ): string { switch (resourceType) { case UniversalResourceType.COMPANIES: return ( SearchUtilities.getFieldValue(record, 'name') || SearchUtilities.getFieldValue(record, 'legal_name') || `Company ${record.id?.record_id ?? ''}`.trim() ); case UniversalResourceType.PEOPLE: return ( SearchUtilities.getFieldValue(record, 'name') || SearchUtilities.getFieldValue(record, 'full_name') || `Person ${record.id?.record_id ?? ''}`.trim() ); case UniversalResourceType.LISTS: return ( SearchUtilities.getFieldValue(record, 'name') || `List ${record.id?.record_id ?? ''}`.trim() ); case UniversalResourceType.TASKS: return ( SearchUtilities.getFieldValue(record, 'content') || SearchUtilities.getFieldValue(record, 'title') || `Task ${record.id?.record_id ?? ''}`.trim() ); default: return `Record ${record.id?.record_id ?? ''}`.trim(); } } function buildSnippet( resourceType: UniversalResourceType, record: AttioRecord ): string | undefined { if (resourceType === UniversalResourceType.COMPANIES) { return firstNonEmpty([ SearchUtilities.getFieldValue(record, 'description'), SearchUtilities.getFieldValue(record, 'about'), SearchUtilities.getFieldValue(record, 'industry'), ]); } if (resourceType === UniversalResourceType.PEOPLE) { return firstNonEmpty([ SearchUtilities.getFieldValue(record, 'job_title'), SearchUtilities.getFieldValue(record, 'role'), SearchUtilities.getFieldValue(record, 'headline'), ]); } if (resourceType === UniversalResourceType.TASKS) { return firstNonEmpty([ SearchUtilities.getFieldValue(record, 'status'), SearchUtilities.getFieldValue(record, 'due_date'), ]); } return undefined; } function firstNonEmpty(values: Array<string | undefined>): string | undefined { return values.find((value) => value && value.trim().length > 0); } function buildApiUrl( resourceType: UniversalResourceType, recordId: string ): string { const path = RESOURCE_API_PATH[resourceType]; if (!path) { return `https://api.attio.com/v2/${recordId}`; } return `https://api.attio.com/v2/${path}/${recordId}`; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server