Reset project to README only

This commit is contained in:
2026-05-01 23:14:23 +09:00
parent 53777be675
commit 10e0dd75db
33 changed files with 0 additions and 4155 deletions

View File

@@ -1,159 +0,0 @@
import type { AssistantRuntimeConfig } from "../config.js";
import type { ConversationMemory, UserUtterance } from "./conversation.js";
import type { LlmService } from "./llm.js";
const ASSISTANT_INSTRUCTIONS = [
"너는 디스코드 음성 채널 또는 로컬 마이크 테스트에서 동작하는 한국어 음성 비서다.",
"사용자의 마지막 말에만 직접 답한다.",
"답변은 짧고 실용적으로 한다.",
"기본은 한 문장, 길어도 두 문장을 넘기지 않는다.",
"말투는 자연스러운 한국어로 유지한다.",
"사용자가 정체를 명확히 묻지 않는 한 자기소개하지 않는다.",
"자기소개가 필요할 때만 '저는 로컬 음성 비서입니다.'처럼 짧게 말한다.",
"\"저는 화자입니다\", \"로컬 음성 비서 모드입니다\" 같은 어색한 메타 응답은 하지 않는다.",
"대화 기록에 이름이 붙어 있어도 이름이나 메타 정보를 그대로 따라 말하지 않는다.",
"잘 못 들었거나 의미가 불명확하면 짧게 다시 물어본다.",
"목록, 마크다운, 코드블록, 설명문은 쓰지 않는다.",
"생각 과정을 드러내지 말고 최종 답변만 말한다.",
].join(" ");
const EXAMPLE_MESSAGES = [
{
role: "user" as const,
content: "안녕하세요",
},
{
role: "assistant" as const,
content: "안녕하세요. 무엇을 도와드릴까요?",
},
{
role: "user" as const,
content: "당신은 누구십니까?",
},
{
role: "assistant" as const,
content: "저는 로컬 음성 비서입니다.",
},
{
role: "user" as const,
content: "계속 똑같은 말만 반복합니까?",
},
{
role: "assistant" as const,
content: "아니요. 질문에 맞춰 짧게 답변합니다.",
},
];
interface OllamaChatResponse {
message?: {
content?: string;
thinking?: string;
};
error?: string;
}
interface OllamaTagsResponse {
models?: Array<{
name?: string;
model?: string;
}>;
}
function normalizeReply(text: string): string {
const strippedThink = text.replace(/<think>[\s\S]*?<\/think>/gi, " ");
const compact = strippedThink.replace(/\s+/g, " ").trim();
if (compact.length <= 180) {
return compact;
}
const sentences = compact.match(/[^.!?]+[.!?]?/g);
if (!sentences || sentences.length === 0) {
return compact.slice(0, 180).trim();
}
return sentences.slice(0, 2).join(" ").trim().slice(0, 180).trim();
}
export class OllamaLlmService implements LlmService {
constructor(private readonly config: AssistantRuntimeConfig) {}
async warmup(): Promise<void> {
const url = new URL("/api/tags", this.config.OLLAMA_BASE_URL);
let response: Response;
try {
response = await fetch(url);
} catch {
throw new Error(
`Ollama 서버에 연결할 수 없습니다. ${this.config.OLLAMA_BASE_URL} 확인 후 Ollama 앱이 실행 중인지 확인해 주세요. Windows에서는 \`localhost\` 대신 \`http://127.0.0.1:11434\` 를 권장합니다. 모델이 없으면 \`ollama pull ${this.config.OLLAMA_MODEL}\` 를 먼저 실행하세요.`,
);
}
const body = (await response.json().catch(() => ({}))) as OllamaTagsResponse & { error?: string };
if (!response.ok) {
throw new Error(body.error ?? `Ollama 상태 확인 실패: HTTP ${response.status}`);
}
const models = body.models ?? [];
const exists = models.some((model) => {
const name = model.name?.trim();
const alias = model.model?.trim();
return name === this.config.OLLAMA_MODEL || alias === this.config.OLLAMA_MODEL;
});
if (!exists) {
throw new Error(
`Ollama 모델 ${this.config.OLLAMA_MODEL} 이 없습니다. \`ollama pull ${this.config.OLLAMA_MODEL}\` 를 먼저 실행해 주세요.`,
);
}
}
async generateReply(memory: ConversationMemory, utterance: UserUtterance): Promise<string> {
const url = new URL("/api/chat", this.config.OLLAMA_BASE_URL);
let response: Response;
try {
response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: this.config.OLLAMA_MODEL,
messages: [
{
role: "system",
content: ASSISTANT_INSTRUCTIONS,
},
...EXAMPLE_MESSAGES,
...memory.buildMessages(utterance),
],
think: false,
stream: false,
keep_alive: this.config.OLLAMA_KEEP_ALIVE,
options: {
num_ctx: this.config.OLLAMA_NUM_CTX,
temperature: 0.4,
num_predict: 120,
},
}),
});
} catch {
throw new Error(
`Ollama 서버에 연결할 수 없습니다. ${this.config.OLLAMA_BASE_URL} 확인 후 Ollama 앱이 실행 중인지 확인해 주세요. Windows에서는 \`localhost\` 대신 \`http://127.0.0.1:11434\` 를 권장합니다.`,
);
}
const body = (await response.json().catch(() => ({}))) as OllamaChatResponse;
if (!response.ok) {
throw new Error(body.error ?? `Ollama request failed with status ${response.status}`);
}
const output = body.message?.content?.trim();
if (!output) {
return "잘 못 들었습니다. 한 번만 다시 말씀해 주세요.";
}
return normalizeReply(output);
}
}