Files
Alpha/src/services/ai.ts
T

194 lines
5.6 KiB
TypeScript

import { PROVIDER_CONFIG, getProviderApiKey, type AIProvider } from '@/stores/settings'
import { storage } from '@/services/storage'
export interface AIExtractedHU {
title: string
description: string
acceptance_criteria: string[]
priority?: string
story_points?: number
type?: 'feature' | 'bug' | 'task' | 'improvement'
}
export interface AIAnalysisResult {
hus: AIExtractedHU[]
summary: string
}
interface ActiveConfig {
baseUrl: string
apiKey: string
model: string
provider: AIProvider
}
function getActiveConfig(): ActiveConfig {
const raw = storage.getJSON<{ provider: AIProvider; modelId: string }>('alpha_settings')
if (raw) {
const provider = raw.provider || 'openrouter'
const model = raw.modelId || 'deepseek/deepseek-chat-v3-0324:free'
const baseUrl = PROVIDER_CONFIG[provider]?.baseUrl || ''
const apiKey = getProviderApiKey(provider)
return { baseUrl, apiKey, model, provider }
}
return {
baseUrl: PROVIDER_CONFIG.openrouter.baseUrl,
apiKey: getProviderApiKey('openrouter'),
model: 'deepseek/deepseek-chat-v3-0324:free',
provider: 'openrouter',
}
}
function buildHeaders(config: ActiveConfig): Record<string, string> {
const h: Record<string, string> = {
'Content-Type': 'application/json',
}
if (config.provider === 'openrouter') {
h['Authorization'] = `Bearer ${config.apiKey}`
h['HTTP-Referer'] = window.location.origin
h['X-Title'] = 'KAPPA Hub Alpha'
} else {
h['Authorization'] = `Bearer ${config.apiKey}`
h['Authorization'] = `Bearer ${config.apiKey}`
}
return h
}
export interface ChatMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
export async function callAI(
messages: ChatMessage[],
temperature = 0.3,
maxTokens = 4096,
signal?: AbortSignal
): Promise<string> {
const config = getActiveConfig()
if (!config.apiKey) {
const label = PROVIDER_CONFIG[config.provider]?.label || config.provider
throw new Error(`No hay API key configurada para ${label}`)
}
if (!config.baseUrl) {
throw new Error(`El proveedor ${config.provider} no tiene API configurada`)
}
console.log(`[Alpha] AI call — provider: ${config.provider}, model: ${config.model}`)
const body: Record<string, unknown> = {
model: config.model,
messages,
temperature,
max_tokens: maxTokens,
}
const res = await fetch(config.baseUrl, {
method: 'POST',
headers: buildHeaders(config),
body: JSON.stringify(body),
signal,
})
const rawText = await res.text()
if (!res.ok) {
throw new Error(`API error (${config.provider}): ${res.status}${rawText.slice(0, 300)}`)
}
const data = JSON.parse(rawText)
const content = data.choices?.[0]?.message?.content || null
if (!content) throw new Error('Respuesta vacía del proveedor de IA')
return stripThinkTags(content)
}
/**
* Elimina bloques de razonamiento interno del modelo (DeepSeek, etc.).
* Ej: "...." o "..."
*/
function stripThinkTags(text: string): string {
return text
.replace(/```thinking[\s\S]*?```/gi, '')
.replace(/```reasoning[\s\S]*?```/gi, '')
.replace(/<think>[\s\S]*?<\/think>/gi, '')
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
.replace(/^.*?\*\*Respuesta:/s, '')
.replace(/^.*?\*\*Final Answer:/s, '')
.trim()
}
export async function chatWithAI(
messages: { role: 'user' | 'assistant'; content: string }[],
systemPrompt?: string,
signal?: AbortSignal
): Promise<string> {
const msgs: ChatMessage[] = []
if (systemPrompt) {
msgs.push({ role: 'system', content: systemPrompt })
}
msgs.push(...messages)
return callAI(msgs, 0.7, 2048, signal)
}
const SYSTEM_PROMPT = `Eres un analista funcional experto en metodologías ágiles. Tu tarea es analizar transcripciones de reuniones y extraer Historias de Usuario (HUs) en formato estructurado.
Reglas:
1. Identifica cada requisito, funcionalidad, bug o mejora mencionada en la transcripción
2. Convierte cada uno en una HU con: título claro, descripción detallada, criterios de aceptación
3. Los criterios de aceptación deben ser verificables (condiciones específicas)
4. Usa el formato "Como [rol] quiero [funcionalidad] para [beneficio]" cuando sea posible
5. Asigna prioridad (Alta/Media/Baja) basada en urgencia implícita
6. No inventes información que no esté en la transcripción
7. Si el texto no contiene información relevante para HUs, devuelve un arreglo vacío
Responde SOLO con JSON válido en este formato:
{
"hus": [
{
"title": "Título de la HU",
"description": "Descripción detallada",
"acceptance_criteria": ["Criterio 1", "Criterio 2"],
"priority": "Alta|Media|Baja",
"story_points": 3,
"type": "feature|bug|task|improvement"
}
],
"summary": "Resumen breve del análisis (2-3 líneas)"
}`
export async function analyzeTranscription(
text: string,
projectName?: string,
signal?: AbortSignal
): Promise<AIAnalysisResult> {
const userContent = projectName
? `Proyecto: ${projectName}\n\nTranscripción:\n${text}`
: `Transcripción:\n${text}`
console.log(`[Alpha] AI analyze — text: ${text.length} chars`)
const content = await callAI(
[
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: userContent },
],
0.3,
4096,
signal,
)
try {
const jsonStr = content.replace(/```json\s*/gi, '').replace(/```\s*$/g, '').trim()
const result: AIAnalysisResult = JSON.parse(jsonStr)
console.log(`[Alpha] AI analysis complete — ${result.hus.length} HUs`)
return result
} catch (e) {
console.error('[Alpha] Failed to parse AI response:', content)
throw new Error('No se pudo parsear la respuesta de la IA')
}
}