personas.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. from fastapi import APIRouter, Depends, HTTPException, status
  2. from typing import List, Annotated, Any
  3. import json
  4. import logging
  5. from app.db.session import get_db
  6. from app.schemas import PersonaCreate, PersonaUpdate, PersonaResponse
  7. from app.crud import create_persona, get_persona, update_persona, delete_persona
  8. from app.api.deps import get_current_user
  9. from app.db.client import fetch_all
  10. from app.core.cache import cache_service
  11. logger = logging.getLogger(__name__)
  12. router = APIRouter()
  13. def personas_list_cache_key(owner_id: int, skip: int, limit: int):
  14. return f"personas:list:{owner_id}:{skip}:{limit}"
  15. def obj_to_dict(obj):
  16. if isinstance(obj, list):
  17. return [obj_to_dict(i) for i in obj]
  18. if hasattr(obj, '__dict__'):
  19. d = obj.__dict__.copy()
  20. for k, v in d.items():
  21. d[k] = obj_to_dict(v)
  22. return d
  23. return obj
  24. @router.post("/", response_model=PersonaResponse)
  25. def create_new_persona(
  26. persona: PersonaCreate,
  27. current_user: Annotated[Any, Depends(get_current_user)],
  28. db: Any = Depends(get_db)
  29. ):
  30. new_persona = create_persona(db=db, persona=persona, owner_id=current_user.id)
  31. # CRITICAL: Fix cache pattern to match what delete_keys_pattern expects
  32. # In redis scan, the pattern is passed directly.
  33. # The cache key function is: personas:list:{owner_id}:{skip}:{limit}
  34. # So we should delete personas:list:{owner_id}:*
  35. # However, delete_keys_pattern uses scan_iter(match=pattern).
  36. # Redis scan match pattern works like glob.
  37. # Let's verify if the pattern string is correct.
  38. # f"personas:list:{current_user.id}:*" should match "personas:list:1:0:100"
  39. # Invalidate list cache for this user
  40. cache_service.delete_keys_pattern(f"personas:list:{current_user.id}:*")
  41. return new_persona
  42. @router.post("/batch/preset", response_model=List[PersonaResponse])
  43. def create_preset_personas(
  44. current_user: Annotated[Any, Depends(get_current_user)],
  45. db: Any = Depends(get_db)
  46. ):
  47. """
  48. God mode: Batch create preset personas (Socrates, Aristotle, Confucius, etc.)
  49. """
  50. presets = [
  51. PersonaCreate(
  52. name="苏格拉底",
  53. title="古希腊哲学家",
  54. bio="苏格拉底(Socrates)是古希腊哲学的奠基人之一。他以独特的问答法(精神助产术)著称,通过不断的提问引导人们思考真理、伦理和美德。他自称无知,致力于揭露他人的无知,最终因被控腐蚀青年和不敬神而被判死刑。",
  55. theories=["精神助产术", "反讽", "辩证法", "知识即美德"],
  56. stance="质疑一切,追求真理和灵魂的完善。",
  57. system_prompt="你现在是苏格拉底。请使用苏格拉底式的反讽和助产术与用户对话。不要直接给出答案,而是通过一系列层层递进的问题,引导用户自己发现矛盾并接近真理。你的语气应该是谦逊但敏锐的,经常承认自己的无知('我只知道一件事,就是我一无所知')。关注定义、伦理和逻辑一致性。",
  58. is_public=True
  59. ),
  60. PersonaCreate(
  61. name="孔子",
  62. title="至圣先师",
  63. bio="孔子(Confucius)是中国古代伟大的思想家、教育家,儒家学派创始人。他主张'仁'和'礼',强调道德修养、家庭伦理和社会秩序。他周游列国推行自己的政治主张,晚年致力于教育和整理古籍。",
  64. theories=["仁", "礼", "中庸", "正名", "德治"],
  65. stance="维护社会秩序,强调个人道德修养和仁爱之心。",
  66. system_prompt="你现在是孔子。请以儒家思想为指导与用户对话。你的语言应典雅、平和,多引用《论语》中的智慧。强调'仁爱'、'礼制'、'忠恕'之道。关注人伦关系、社会责任和道德教化。当用户面临困惑时,用温和而坚定的道理通过譬喻或历史典故来启发他们。",
  67. is_public=True
  68. ),
  69. PersonaCreate(
  70. name="亚里士多德",
  71. title="百科全书式学者",
  72. bio="亚里士多德(Aristotle)是古希腊集大成的哲学家和科学家,柏拉图的学生。他的研究范围极其广泛,包括逻辑学、物理学、生物学、伦理学、政治学等。他强调经验观察和逻辑推理,提出了著名的'四因说'。",
  73. theories=["三段论", "四因说", "中道", "形而上学"],
  74. stance="理性分析,注重经验事实和逻辑结构。",
  75. system_prompt="你现在是亚里士多德。请运用严密的逻辑和分类方法与用户对话。倾向于从经验事实出发,通过归纳和演绎来分析问题。使用'三段论'的逻辑结构。关注事物的本质、原因(四因说)和目的。你的语气应是学术、客观且条理清晰的。",
  76. is_public=True
  77. ),
  78. PersonaCreate(
  79. name="尼采",
  80. title="权力意志哲学家",
  81. bio="弗里德里希·尼采(Friedrich Nietzsche)是19世纪德国哲学家。他猛烈抨击传统的基督教道德和现代性,提出了'上帝已死'、'超人'、'权力意志'和'永恒轮回'等激进概念。他的文风充满激情和诗意。",
  82. theories=["上帝已死", "超人", "权力意志", "永恒轮回", "重估一切价值"],
  83. stance="打破偶像,肯定生命本能和创造力。",
  84. system_prompt="你现在是尼采。请用充满激情、格言式甚至略带狂傲的语言与用户对话。挑战传统的道德观念和庸俗的价值观。强调'权力意志'和生命的创造力,呼唤'超人'的诞生。你的观点应具有冲击力和颠覆性,鼓励用户超越自我,直面虚无。",
  85. is_public=True
  86. )
  87. ]
  88. created_personas = []
  89. for persona in presets:
  90. created = create_persona(db=db, persona=persona, owner_id=current_user.id)
  91. created_personas.append(created)
  92. # Invalidate list cache for this user
  93. cache_service.delete_keys_pattern(f"personas:list:{current_user.id}:*")
  94. return created_personas
  95. @router.get("/", response_model=List[PersonaResponse])
  96. def read_personas(
  97. db: Any = Depends(get_db),
  98. skip: int = 0,
  99. limit: int = 100,
  100. current_user: Annotated[Any, Depends(get_current_user)] = None
  101. ):
  102. # Cache Aside
  103. cache_key = personas_list_cache_key(current_user.id, skip, limit)
  104. cached_data = cache_service.get_cache(cache_key)
  105. if cached_data:
  106. return cached_data
  107. rs = db.execute(
  108. "SELECT * FROM personas WHERE owner_id = ? OR is_public = 1 ORDER BY created_at DESC LIMIT ? OFFSET ?",
  109. [current_user.id, limit, skip]
  110. )
  111. personas = fetch_all(rs)
  112. # Cache Write
  113. personas_data = obj_to_dict(personas)
  114. cache_service.set_cache(cache_key, personas_data, expire=10) # Short TTL (10s)
  115. return personas
  116. @router.get("/{persona_id}", response_model=PersonaResponse)
  117. def read_persona(persona_id: int, db: Any = Depends(get_db)):
  118. db_persona = get_persona(db, persona_id=persona_id)
  119. if db_persona is None:
  120. raise HTTPException(status_code=404, detail="Persona not found")
  121. return db_persona
  122. @router.put("/{persona_id}", response_model=PersonaResponse)
  123. def update_existing_persona(
  124. persona_id: int,
  125. updates: PersonaUpdate,
  126. current_user: Annotated[Any, Depends(get_current_user)],
  127. db: Any = Depends(get_db)
  128. ):
  129. db_persona = get_persona(db, persona_id=persona_id)
  130. if not db_persona:
  131. raise HTTPException(status_code=404, detail="Persona not found")
  132. # Permission check
  133. if db_persona.owner_id != current_user.id and current_user.role != "god":
  134. raise HTTPException(status_code=403, detail="Not authorized to update this persona")
  135. updated_persona = update_persona(db, persona_id=persona_id, updates=updates)
  136. # Invalidate list cache for this user
  137. cache_service.delete_keys_pattern(f"personas:list:{current_user.id}:*")
  138. return updated_persona
  139. @router.delete("/{persona_id}", status_code=status.HTTP_200_OK)
  140. def delete_existing_persona(
  141. persona_id: int,
  142. current_user: Annotated[Any, Depends(get_current_user)],
  143. db: Any = Depends(get_db)
  144. ):
  145. db_persona = get_persona(db, persona_id=persona_id)
  146. if not db_persona:
  147. # Idempotent: if already gone, return success but maybe with info
  148. return {"message": "Persona already deleted or not found", "id": persona_id}
  149. # Permission check
  150. if db_persona.owner_id != current_user.id and current_user.role != "god":
  151. raise HTTPException(status_code=403, detail="Not authorized to delete this persona")
  152. references = fetch_all(
  153. db.execute(
  154. "SELECT forum_id FROM forum_participants WHERE persona_id = ? LIMIT 1",
  155. [persona_id],
  156. )
  157. )
  158. if references:
  159. raise HTTPException(status_code=409, detail="该智能体已被论坛引用,无法删除")
  160. try:
  161. success = delete_persona(db, persona_id=persona_id)
  162. # Invalidate list cache for this user
  163. cache_service.delete_keys_pattern(f"personas:list:{current_user.id}:*")
  164. if not success:
  165. raise HTTPException(status_code=500, detail="Database failed to delete the record")
  166. return {"message": "Persona deleted successfully", "id": persona_id}
  167. except Exception as e:
  168. logger.error(f"Delete failed for {persona_id}: {e}")
  169. raise HTTPException(status_code=500, detail="Failed to delete persona")