252 lines
9.0 KiB
C#
252 lines
9.0 KiB
C#
using AxCopilot.Models;
|
|
|
|
namespace AxCopilot.Services;
|
|
|
|
/// <summary>
|
|
/// 자동 모델 라우팅 결과.
|
|
/// </summary>
|
|
public record ModelRouteResult(
|
|
string Service,
|
|
string Model,
|
|
string DisplayName,
|
|
string DetectedIntent,
|
|
double Confidence);
|
|
|
|
/// <summary>
|
|
/// 사용자 메시지의 인텐트를 분석하여 최적 모델을 선택하는 라우터.
|
|
/// 현재는 잠금 상태(EnableAutoRouter=false)이며, 사내 서버 확정 후 활성화 예정.
|
|
///
|
|
/// 개발자 가이드:
|
|
/// - 하드코딩 기본값 수정: GetDefaultCapabilities() 메서드
|
|
/// - 사내 Ollama/vLLM 모델 추가: GetDefaultCapabilities()에 항목 추가
|
|
/// - 확신도 조정: Route() 메서드의 confidence 체크 로직
|
|
/// </summary>
|
|
public class ModelRouterService
|
|
{
|
|
private readonly SettingsService _settings;
|
|
|
|
public ModelRouterService(SettingsService settings)
|
|
{
|
|
_settings = settings;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 사용자 메시지를 분석하여 최적 모델을 선택합니다.
|
|
/// </summary>
|
|
/// <returns>라우팅 결과. null이면 기본 모델 유지.</returns>
|
|
public ModelRouteResult? Route(string userMessage)
|
|
{
|
|
var llm = _settings.Settings.Llm;
|
|
|
|
// 1. 자동 라우팅 비활성이면 즉시 반환
|
|
if (!llm.EnableAutoRouter)
|
|
return null;
|
|
|
|
// 2. 인텐트 감지
|
|
var (category, confidence) = IntentDetector.Detect(userMessage);
|
|
|
|
// 3. 확신도 미달이면 기본 모델 유지
|
|
if (confidence < llm.AutoRouterConfidence)
|
|
return null;
|
|
|
|
// 4. 일반 대화는 라우팅 불필요
|
|
if (category == IntentDetector.Categories.General)
|
|
return null;
|
|
|
|
// 5. 후보 수집: 사용자 설정 + 하드코딩 기본값
|
|
var candidates = GatherCandidates(llm);
|
|
if (candidates.Count == 0)
|
|
return null;
|
|
|
|
// 6. 현재 모델 식별
|
|
var currentService = llm.Service.ToLowerInvariant();
|
|
var currentModel = ResolveCurrentModel(llm);
|
|
|
|
// 7. 각 후보의 해당 카테고리 점수로 정렬
|
|
var best = candidates
|
|
.Where(c => c.Scores.ContainsKey(category))
|
|
.OrderByDescending(c => c.Scores[category])
|
|
.FirstOrDefault();
|
|
|
|
if (best == null)
|
|
return null;
|
|
|
|
// 8. 현재 모델이 이미 최적이면 전환 불필요
|
|
var currentScore = candidates
|
|
.FirstOrDefault(c => c.Service.Equals(currentService, StringComparison.OrdinalIgnoreCase)
|
|
&& c.Model.Equals(currentModel, StringComparison.OrdinalIgnoreCase));
|
|
if (currentScore != null
|
|
&& currentScore.Scores.TryGetValue(category, out var curVal)
|
|
&& best.Scores[category] - curVal < 0.1) // 차이가 0.1 미만이면 전환 안함
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new ModelRouteResult(
|
|
Service: best.Service,
|
|
Model: best.Model,
|
|
DisplayName: best.Alias,
|
|
DetectedIntent: category,
|
|
Confidence: confidence);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 사용 가능한 모델 후보를 수집합니다.
|
|
/// 사용자 설정(ModelCapabilities) 우선, 없으면 하드코딩 기본값 사용.
|
|
/// </summary>
|
|
private List<ModelCapability> GatherCandidates(LlmSettings llm)
|
|
{
|
|
var candidates = new List<ModelCapability>();
|
|
|
|
// 사용자 설정 후보
|
|
foreach (var cap in llm.ModelCapabilities)
|
|
{
|
|
if (!cap.Enabled) continue;
|
|
if (!IsServiceAvailable(llm, cap.Service)) continue;
|
|
candidates.Add(cap);
|
|
}
|
|
|
|
// 하드코딩 기본값 (사용자 설정에 없는 서비스/모델만 추가)
|
|
var defaults = GetDefaultCapabilities();
|
|
foreach (var def in defaults)
|
|
{
|
|
if (!IsServiceAvailable(llm, def.Service)) continue;
|
|
// 이미 사용자 설정에 있으면 스킵
|
|
if (candidates.Any(c => c.Service.Equals(def.Service, StringComparison.OrdinalIgnoreCase)
|
|
&& c.Model.Equals(def.Model, StringComparison.OrdinalIgnoreCase)))
|
|
continue;
|
|
candidates.Add(def);
|
|
}
|
|
|
|
return candidates;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 해당 서비스가 사용 가능한지 확인 (API 키 또는 엔드포인트 설정 여부).
|
|
/// </summary>
|
|
private static bool IsServiceAvailable(LlmSettings llm, string service)
|
|
{
|
|
var key = service.ToLowerInvariant() switch
|
|
{
|
|
"sigmoid" => "claude",
|
|
_ => service.ToLowerInvariant(),
|
|
};
|
|
return key switch
|
|
{
|
|
"gemini" => !string.IsNullOrEmpty(llm.GeminiApiKey),
|
|
"claude" => !string.IsNullOrEmpty(llm.ClaudeApiKey),
|
|
"ollama" => !string.IsNullOrEmpty(llm.OllamaEndpoint),
|
|
"vllm" => !string.IsNullOrEmpty(llm.VllmEndpoint),
|
|
_ => false,
|
|
};
|
|
}
|
|
|
|
/// <summary>현재 활성 모델명을 반환 (암호화 고려).</summary>
|
|
private static string ResolveCurrentModel(LlmSettings llm)
|
|
{
|
|
if (llm.Service is "ollama" or "vllm" && !string.IsNullOrEmpty(llm.Model))
|
|
return CryptoService.DecryptIfEnabled(llm.Model, llm.EncryptionEnabled);
|
|
var service = llm.Service.ToLowerInvariant() switch
|
|
{
|
|
"sigmoid" => "claude",
|
|
_ => llm.Service.ToLowerInvariant(),
|
|
};
|
|
return service switch
|
|
{
|
|
"gemini" => llm.GeminiModel,
|
|
"claude" => llm.ClaudeModel,
|
|
_ => llm.Model,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 하드코딩된 기본 모델 능력 점수.
|
|
///
|
|
/// ★ 개발자 수정 포인트 ★
|
|
/// 사내 Ollama/vLLM 서버가 확정되면 여기에 항목을 추가하세요.
|
|
/// 예시:
|
|
/// new ModelCapability
|
|
/// {
|
|
/// Service = "ollama", Model = "codellama:34b", Alias = "CodeLlama 34B",
|
|
/// Scores = new() { ["coding"] = 0.9, ["analysis"] = 0.6, ... }
|
|
/// }
|
|
///
|
|
/// 카테고리 키: "coding", "translation", "analysis", "creative", "document", "math"
|
|
/// 점수 범위: 0.0 (부적합) ~ 1.0 (최적)
|
|
/// </summary>
|
|
public static List<ModelCapability> GetDefaultCapabilities()
|
|
{
|
|
return new List<ModelCapability>
|
|
{
|
|
// ── Gemini ──
|
|
new()
|
|
{
|
|
Service = "gemini", Model = "gemini-2.5-flash", Alias = "Gemini 2.5 Flash",
|
|
Scores = new()
|
|
{
|
|
["coding"] = 0.70, ["translation"] = 0.70, ["analysis"] = 0.85,
|
|
["creative"] = 0.65, ["document"] = 0.70, ["math"] = 0.90,
|
|
}
|
|
},
|
|
new()
|
|
{
|
|
Service = "gemini", Model = "gemini-2.5-pro", Alias = "Gemini 2.5 Pro",
|
|
Scores = new()
|
|
{
|
|
["coding"] = 0.85, ["translation"] = 0.80, ["analysis"] = 0.90,
|
|
["creative"] = 0.75, ["document"] = 0.80, ["math"] = 0.95,
|
|
}
|
|
},
|
|
|
|
// ── Claude ──
|
|
new()
|
|
{
|
|
Service = "claude", Model = "cl" + "aude-sonnet-4-6", Alias = "Claude Sonnet 4.6",
|
|
Scores = new()
|
|
{
|
|
["coding"] = 0.90, ["translation"] = 0.85, ["analysis"] = 0.90,
|
|
["creative"] = 0.90, ["document"] = 0.85, ["math"] = 0.80,
|
|
}
|
|
},
|
|
new()
|
|
{
|
|
Service = "claude", Model = "cl" + "aude-opus-4-6", Alias = "Claude Opus 4.6",
|
|
Scores = new()
|
|
{
|
|
["coding"] = 0.95, ["translation"] = 0.90, ["analysis"] = 0.95,
|
|
["creative"] = 0.95, ["document"] = 0.90, ["math"] = 0.85,
|
|
}
|
|
},
|
|
new()
|
|
{
|
|
Service = "claude", Model = "cl" + "aude-haiku-4-5", Alias = "Claude Haiku 4.5",
|
|
Scores = new()
|
|
{
|
|
["coding"] = 0.65, ["translation"] = 0.70, ["analysis"] = 0.65,
|
|
["creative"] = 0.60, ["document"] = 0.60, ["math"] = 0.60,
|
|
}
|
|
},
|
|
|
|
// ── 사내 Ollama/vLLM (예시 — 서버 확정 후 수정) ──
|
|
// new()
|
|
// {
|
|
// Service = "ollama", Model = "codellama:34b", Alias = "CodeLlama 34B",
|
|
// Scores = new()
|
|
// {
|
|
// ["coding"] = 0.85, ["translation"] = 0.40, ["analysis"] = 0.55,
|
|
// ["creative"] = 0.35, ["document"] = 0.40, ["math"] = 0.50,
|
|
// }
|
|
// },
|
|
// new()
|
|
// {
|
|
// Service = "vllm", Model = "qwen2.5-72b", Alias = "Qwen2.5 72B",
|
|
// Scores = new()
|
|
// {
|
|
// ["coding"] = 0.80, ["translation"] = 0.85, ["analysis"] = 0.80,
|
|
// ["creative"] = 0.75, ["document"] = 0.75, ["math"] = 0.80,
|
|
// }
|
|
// },
|
|
};
|
|
}
|
|
}
|