|
@@ -0,0 +1,176 @@
|
|
|
+import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
|
|
+import axios, { AxiosResponse } from 'axios';
|
|
|
+import { OllamaChatDto, OllamaGenerateDto } from './dto/ollama-chat.dto';
|
|
|
+
|
|
|
+export interface OllamaResponse {
|
|
|
+ model: string;
|
|
|
+ created_at: string;
|
|
|
+ message: {
|
|
|
+ role: string;
|
|
|
+ content: string;
|
|
|
+ };
|
|
|
+ done: boolean;
|
|
|
+ total_duration: number;
|
|
|
+ load_duration: number;
|
|
|
+ prompt_eval_count: number;
|
|
|
+ prompt_eval_duration: number;
|
|
|
+ eval_count: number;
|
|
|
+ eval_duration: number;
|
|
|
+}
|
|
|
+
|
|
|
+export interface OllamaGenerateResponse {
|
|
|
+ model: string;
|
|
|
+ created_at: string;
|
|
|
+ response: string;
|
|
|
+ done: boolean;
|
|
|
+ context: number[];
|
|
|
+ total_duration: number;
|
|
|
+ load_duration: number;
|
|
|
+ prompt_eval_count: number;
|
|
|
+ prompt_eval_duration: number;
|
|
|
+ eval_count: number;
|
|
|
+ eval_duration: number;
|
|
|
+}
|
|
|
+
|
|
|
+@Injectable()
|
|
|
+export class OllamaService {
|
|
|
+ private readonly baseUrl: string;
|
|
|
+
|
|
|
+ constructor() {
|
|
|
+ // 默认 Ollama 服务地址,可以通过环境变量配置
|
|
|
+ this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 与 Ollama 进行聊天对话
|
|
|
+ */
|
|
|
+ async chat(chatDto: OllamaChatDto): Promise<OllamaResponse> {
|
|
|
+ try {
|
|
|
+ const response: AxiosResponse<OllamaResponse> = await axios.post(
|
|
|
+ `${this.baseUrl}/api/chat`,
|
|
|
+ {
|
|
|
+ model: chatDto.model || 'llama2',
|
|
|
+ messages: chatDto.messages,
|
|
|
+ stream: chatDto.stream || false,
|
|
|
+ options: {
|
|
|
+ temperature: chatDto.temperature || 0.7,
|
|
|
+ num_predict: chatDto.max_tokens || 2048,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ timeout: 30000, // 30秒超时
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ return response.data;
|
|
|
+ } catch (error) {
|
|
|
+ if (axios.isAxiosError(error)) {
|
|
|
+ if (error.code === 'ECONNREFUSED') {
|
|
|
+ throw new HttpException(
|
|
|
+ '无法连接到 Ollama 服务,请确保 Ollama 正在运行',
|
|
|
+ HttpStatus.SERVICE_UNAVAILABLE,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (error.response) {
|
|
|
+ throw new HttpException(
|
|
|
+ `Ollama 服务错误: ${error.response.data?.error || error.message}`,
|
|
|
+ error.response.status,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new HttpException(
|
|
|
+ `调用 Ollama 服务失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
|
|
+ HttpStatus.INTERNAL_SERVER_ERROR,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用 Ollama 生成文本
|
|
|
+ */
|
|
|
+ async generate(
|
|
|
+ generateDto: OllamaGenerateDto,
|
|
|
+ ): Promise<OllamaGenerateResponse> {
|
|
|
+ try {
|
|
|
+ const response: AxiosResponse<OllamaGenerateResponse> = await axios.post(
|
|
|
+ `${this.baseUrl}/api/generate`,
|
|
|
+ {
|
|
|
+ model: generateDto.model,
|
|
|
+ prompt: generateDto.prompt,
|
|
|
+ stream: generateDto.stream || false,
|
|
|
+ options: {
|
|
|
+ temperature: generateDto.temperature || 0.7,
|
|
|
+ num_predict: generateDto.max_tokens || 2048,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ },
|
|
|
+ timeout: 30000, // 30秒超时
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ return response.data;
|
|
|
+ } catch (error) {
|
|
|
+ if (axios.isAxiosError(error)) {
|
|
|
+ if (error.code === 'ECONNREFUSED') {
|
|
|
+ throw new HttpException(
|
|
|
+ '无法连接到 Ollama 服务,请确保 Ollama 正在运行',
|
|
|
+ HttpStatus.SERVICE_UNAVAILABLE,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (error.response) {
|
|
|
+ throw new HttpException(
|
|
|
+ `Ollama 服务错误: ${error.response.data?.error || error.message}`,
|
|
|
+ error.response.status,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new HttpException(
|
|
|
+ `调用 Ollama 服务失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
|
|
+ HttpStatus.INTERNAL_SERVER_ERROR,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取可用的模型列表
|
|
|
+ */
|
|
|
+ async listModels(): Promise<any> {
|
|
|
+ try {
|
|
|
+ const response: AxiosResponse = await axios.get(
|
|
|
+ `${this.baseUrl}/api/tags`,
|
|
|
+ );
|
|
|
+ return response.data;
|
|
|
+ } catch (error) {
|
|
|
+ if (axios.isAxiosError(error)) {
|
|
|
+ if (error.code === 'ECONNREFUSED') {
|
|
|
+ throw new HttpException(
|
|
|
+ '无法连接到 Ollama 服务,请确保 Ollama 正在运行',
|
|
|
+ HttpStatus.SERVICE_UNAVAILABLE,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new HttpException(
|
|
|
+ `获取模型列表失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
|
|
+ HttpStatus.INTERNAL_SERVER_ERROR,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查 Ollama 服务状态
|
|
|
+ */
|
|
|
+ async healthCheck(): Promise<boolean> {
|
|
|
+ try {
|
|
|
+ await axios.get(`${this.baseUrl}/api/tags`, { timeout: 5000 });
|
|
|
+ return true;
|
|
|
+ } catch (_error) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|