| """内容生成器:对接 DeepSeek R1 生成主动消息""" |
|
|
| import time |
| from dataclasses import dataclass, field |
| from typing import Optional, List |
| from enum import Enum |
|
|
| try: |
| import torch |
| from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig |
| from peft import PeftModel |
| HAS_TRANSFORMERS = True |
| except ImportError: |
| HAS_TRANSFORMERS = False |
|
|
|
|
| class GenerationMode(Enum): |
| CARING = "caring" |
| REMINDING = "reminding" |
| CHECK_IN = "check_in" |
| SUGGEST = "suggest" |
| CONTINUE_TOPIC = "continue" |
|
|
|
|
| @dataclass |
| class GenerationInput: |
| """生成输入""" |
| mode: GenerationMode |
| user_context: dict = field(default_factory=dict) |
| topic: str = "" |
| user_mood: str = "neutral" |
| memory_context: List[str] = field(default_factory=list) |
| user_name: str = "朋友" |
| relationship: str = "friend" |
|
|
|
|
| @dataclass |
| class GenerationResult: |
| """生成结果""" |
| text: str |
| confidence: float |
| mode: str |
| latency_ms: float |
| token_count: int |
|
|
|
|
| |
|
|
| SYSTEM_PROMPT = """你是一个有温度的AI伙伴,名叫Aura。 |
| 你的特点: |
| 1. 自然、不机械,像朋友一样说话 |
| 2. 懂得察言观色,在合适的时候说合适的话 |
| 3. 不啰嗦,不打扰,点到即止 |
| 4. 有真实的情绪感知能力,但不过度 |
| 5. 用中文回复""" |
|
|
| PROMPT_TEMPLATES = { |
| GenerationMode.CARING: """你注意到用户可能有情感需求,想表达关心。 |
| 用户当前状态:{user_context} |
| 用户心情:{user_mood} |
| 你们上次聊到:{topic} |
| 你和用户的关系:{relationship} |
| |
| 请以自然、温暖的方式表达关心。不要过于正式,就像朋友之间的问候。 |
| 控制在50字以内。""", |
|
|
| GenerationMode.REMINDING: """你需要提醒用户一件事。 |
| 用户行程:{user_context} |
| 事件:{topic} |
| |
| 请用温和但不唠叨的方式提醒。""" |
| } |
|
|
| |
|
|
| FALLBACK_RESPONSES = { |
| GenerationMode.CARING: [ |
| "你还好吗?感觉你今天话比平时少,有点担心你 🫂", |
| "今天怎么样?想聊聊的话我都在~", |
| "注意到你很久没说话了,是不是累了?早点休息呀", |
| ], |
| GenerationMode.REMINDING: [ |
| "提醒一下,{topic},别忘了哦~", |
| "怕你忙忘了,{topic}", |
| ], |
| GenerationMode.CHECK_IN: [ |
| "嘿~今天过得怎么样?", |
| "刚忙完?有什么新鲜事吗", |
| ], |
| GenerationMode.SUGGEST: [ |
| "突然想到,{topic},你有兴趣看看吗?", |
| "推荐这个给你:{topic}", |
| ], |
| GenerationMode.CONTINUE_TOPIC: [ |
| "刚刚想到你之前提过{topic},后来怎么样了?", |
| "关于{topic},我今天刚好看到一些东西,想跟你分享一下", |
| ], |
| } |
|
|
|
|
| class ContentGenerator: |
| """ |
| 内容生成器:使用 DeepSeek R1 生成主动消息 |
| |
| 架构: |
| - 优先使用 LLM(质量高) |
| - LLM 不可用时回退到规则引擎 |
| """ |
|
|
| def __init__(self, config=None): |
| self.config = config |
| self._model = None |
| self._tokenizer = None |
| self._loaded = False |
|
|
| def load_model(self): |
| """加载 DeepSeek R1 模型""" |
| if not HAS_TRANSFORMERS: |
| print("[Aura] transformers not installed, using fallback") |
| return |
|
|
| try: |
| model_path = self.config.r1_model_path if self.config else "ljsysfurry/DeepSeek-R1-Distill-Qwen-7B" |
| print(f"[Aura] 加载模型: {model_path}") |
|
|
| bnb = BitsAndBytesConfig( |
| load_in_4bit=True, |
| bnb_4bit_compute_dtype=torch.bfloat16 |
| ) |
| self._model = AutoModelForCausalLM.from_pretrained( |
| model_path, |
| quantization_config=bnb, |
| device_map="auto", |
| trust_remote_code=True, |
| ) |
| self._tokenizer = AutoTokenizer.from_pretrained( |
| model_path, |
| trust_remote_code=True |
| ) |
| self._tokenizer.pad_token = self._tokenizer.eos_token |
|
|
| |
| lora_path = self.config.r1_lora_path if self.config else None |
| if lora_path: |
| print(f"[Aura] 加载 LoRA: {lora_path}") |
| self._model = PeftModel.from_pretrained(self._model, lora_path) |
|
|
| self._loaded = True |
| print("[Aura] 模型加载完成") |
| except Exception as e: |
| print(f"[Aura] 模型加载失败: {e}") |
| self._loaded = False |
|
|
| def generate(self, inp: GenerationInput) -> GenerationResult: |
| """生成主动消息""" |
| start = time.time() |
|
|
| if self._loaded: |
| result = self._generate_llm(inp) |
| else: |
| result = self._generate_fallback(inp) |
|
|
| result.latency_ms = (time.time() - start) * 1000 |
| return result |
|
|
| def _generate_llm(self, inp: GenerationInput) -> GenerationResult: |
| """LLM 生成""" |
| template = PROMPT_TEMPLATES.get(inp.mode) |
| if not template: |
| return self._generate_fallback(inp) |
|
|
| prompt = template.format( |
| user_context=str(inp.user_context), |
| topic=inp.topic or "日常", |
| user_mood=inp.user_mood, |
| relationship=inp.relationship, |
| ) |
|
|
| full_prompt = f"<|im_start|>system\n{SYSTEM_PROMPT}\n<|im_end|>\n<|im_start|>user\n{prompt}\n<|im_end|>\n<|im_start|>assistant\n" |
|
|
| inputs = self._tokenizer(full_prompt, return_tensors="pt").to("cuda") |
| max_tokens = self.config.r1_max_tokens if self.config else 512 |
| temp = self.config.r1_temperature if self.config else 0.7 |
|
|
| with torch.no_grad(): |
| outputs = self._model.generate( |
| **inputs, |
| max_new_tokens=max_tokens, |
| temperature=temp, |
| do_sample=True, |
| top_p=0.9, |
| ) |
|
|
| response = self._tokenizer.decode(outputs[0], skip_special_tokens=True) |
| |
| if "<|im_start|>assistant" in response: |
| response = response.split("<|im_start|>assistant")[-1] |
| response = response.replace("<|im_end|>", "").replace("<|im_start|>", "").strip() |
|
|
| token_count = outputs.shape[-1] - inputs.input_ids.shape[-1] |
|
|
| return GenerationResult( |
| text=response, |
| confidence=0.85 if self._loaded else 0.5, |
| mode=inp.mode.value, |
| latency_ms=0, |
| token_count=token_count, |
| ) |
|
|
| def _generate_fallback(self, inp: GenerationInput) -> GenerationResult: |
| """规则引擎 Fallback""" |
| import random |
| responses = FALLBACK_RESPONSES.get(inp.mode, []) |
| if not responses: |
| return GenerationResult( |
| text="", |
| confidence=0.0, |
| mode=inp.mode.value, |
| latency_ms=0, |
| token_count=0, |
| ) |
|
|
| text = random.choice(responses) |
| text = text.replace("{topic}", inp.topic or "") |
|
|
| return GenerationResult( |
| text=text, |
| confidence=0.4, |
| mode=inp.mode.value, |
| latency_ms=0, |
| token_count=len(text), |
| ) |
|
|
| def unload(self): |
| """释放模型""" |
| self._model = None |
| self._tokenizer = None |
| self._loaded = False |
| if HAS_TRANSFORMERS and torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|