1
0

forum_service.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. from typing import Any
  2. from datetime import datetime, timezone
  3. from app.crud import (
  4. create_forum,
  5. get_forum,
  6. create_message,
  7. get_forum_messages,
  8. get_persona,
  9. delete_forum,
  10. get_forum_participants,
  11. update_forum,
  12. )
  13. from app.schemas import ForumCreate, MessageCreate
  14. from app.core.websockets import manager
  15. from app.services.forum_scheduler import scheduler
  16. from app.agent.agent import ParticipantAgent
  17. from fastapi import HTTPException
  18. class ForumService:
  19. def __init__(self, db: Any):
  20. self.db = db
  21. def create_new_forum(self, forum_in: ForumCreate, creator_id: int):
  22. forum_in.participant_ids = list(dict.fromkeys(int(pid) for pid in forum_in.participant_ids))
  23. if forum_in.participant_ids:
  24. for pid in forum_in.participant_ids:
  25. p = get_persona(self.db, pid)
  26. if not p:
  27. raise HTTPException(status_code=404, detail=f"Persona {pid} not found")
  28. if p.owner_id != creator_id and not p.is_public:
  29. raise HTTPException(status_code=403, detail="不能邀请其他用户的私有智能体")
  30. if forum_in.moderator_id:
  31. rs = self.db.execute("SELECT 1 FROM moderators WHERE id = ?", [forum_in.moderator_id])
  32. # Check if any row is returned
  33. # LibSQL sync client result object has rows property which is a list of tuples
  34. # Or fetchone method if wrapped
  35. from app.db.client import fetch_one
  36. if not fetch_one(rs):
  37. raise HTTPException(status_code=404, detail=f"Moderator {forum_in.moderator_id} not found")
  38. return create_forum(self.db, forum_in, creator_id)
  39. async def start_forum(self, forum_id: int, user_id: int, is_admin: bool = False, ablation_flags: dict = None):
  40. forum = get_forum(self.db, forum_id)
  41. if not forum:
  42. raise HTTPException(status_code=404, detail="Forum not found")
  43. if forum.creator_id != user_id and not is_admin:
  44. raise HTTPException(status_code=403, detail="Not authorized")
  45. if forum.status == "running":
  46. return {
  47. "status": "already_running",
  48. "ablation_flags": ablation_flags or {},
  49. "start_time": forum.start_time,
  50. "duration_minutes": forum.duration_minutes or 30,
  51. }
  52. flags = ablation_flags or {}
  53. # A non-running forum always starts a fresh session. This also repairs
  54. # legacy pending rows whose creation timestamp was stored as start_time.
  55. started_at = datetime.now(timezone.utc)
  56. update_forum(self.db, forum_id, status="running", start_time=started_at, ablation_flags=flags)
  57. await scheduler.start_forum(forum_id, flags)
  58. return {"status": "started", "ablation_flags": flags, "start_time": started_at, "duration_minutes": forum.duration_minutes or 30}
  59. async def delete_forum(self, forum_id: int, user_id: int, is_admin: bool = False):
  60. forum = get_forum(self.db, forum_id)
  61. if not forum:
  62. # If not found, maybe already deleted, return True to be idempotent
  63. return True
  64. if forum.creator_id != user_id and not is_admin:
  65. raise HTTPException(status_code=403, detail="Not authorized")
  66. # Stop any running tasks for this forum first
  67. try:
  68. await scheduler.stop_forum(forum_id)
  69. except Exception as e:
  70. # Log error but proceed with deletion
  71. import logging
  72. logging.getLogger(__name__).error(f"Error stopping forum {forum_id} before delete: {e}")
  73. # Clear cache related to this forum
  74. try:
  75. from app.core.cache import cache_service
  76. cache_service.delete_keys_pattern(f"forums:list:{user_id}:*")
  77. # If forum has participants, clear their cache if needed? No, participant list cache isn't global.
  78. except:
  79. pass
  80. # Ensure we use a new transaction/connection for deletion if needed,
  81. # but self.db is injected.
  82. return delete_forum(self.db, forum_id)
  83. async def stop_forum(self, forum_id: int, user_id: int, is_admin: bool = False):
  84. forum = get_forum(self.db, forum_id)
  85. if not forum:
  86. raise HTTPException(status_code=404, detail="Forum not found")
  87. if forum.creator_id != user_id and not is_admin:
  88. raise HTTPException(status_code=403, detail="Not authorized")
  89. if forum.status in {"closed", "finished"}:
  90. return {"status": "closed"}
  91. await scheduler.stop_forum(forum_id)
  92. return {"status": "closed"}
  93. async def post_message(self, forum_id: int, msg_in: MessageCreate):
  94. if msg_in.forum_id != forum_id:
  95. raise HTTPException(status_code=400, detail="Forum ID mismatch")
  96. forum = get_forum(self.db, forum_id)
  97. if not forum:
  98. raise HTTPException(status_code=404, detail="Forum not found")
  99. if msg_in.persona_id:
  100. p = get_persona(self.db, msg_in.persona_id)
  101. if not p:
  102. raise HTTPException(status_code=404, detail="Persona not found")
  103. # Calculate turn count if not provided?
  104. # Current logic trusts frontend, but better to count from DB.
  105. # messages = get_forum_messages(self.db, forum_id)
  106. # msg_in.turn_count = len(messages) + 1
  107. new_msg = create_message(self.db, msg_in)
  108. # RowObject or dict doesn't have .isoformat() if timestamp is string
  109. # libsql returns DATETIME as string usually.
  110. # We need to handle this.
  111. # If new_msg is RowObject, timestamp is likely a string "YYYY-MM-DD HH:MM:SS"
  112. ts = new_msg.timestamp
  113. # Check if ts is string
  114. if not isinstance(ts, str) and hasattr(ts, 'isoformat'):
  115. ts = ts.isoformat()
  116. await manager.broadcast(forum_id, {
  117. "type": "new_message",
  118. "data": {
  119. "id": new_msg.id,
  120. "forum_id": forum_id,
  121. "speaker_name": new_msg.speaker_name,
  122. "content": new_msg.content,
  123. "persona_id": new_msg.persona_id,
  124. "timestamp": ts
  125. }
  126. })
  127. return new_msg