22
33import django
44from django .contrib .auth import get_user_model
5+ from django .core .exceptions import ValidationError
56from django .db import models
6- from django .forms import ValidationError
77from guardian .models import GroupObjectPermissionBase , UserObjectPermissionBase
88
99from opencontractserver .annotations .models import Annotation
1010from opencontractserver .corpuses .models import Corpus
1111from opencontractserver .documents .models import Document
1212from opencontractserver .shared .defaults import jsonfield_default_value
1313from opencontractserver .shared .fields import NullableJSONField
14+ from opencontractserver .shared .Managers import BaseVisibilityManager
1415from opencontractserver .shared .Models import BaseOCModel
1516
1617User = get_user_model ()
@@ -29,6 +30,82 @@ class MessageStateChoices(models.TextChoices):
2930 AWAITING_APPROVAL = "awaiting_approval" , "Awaiting Approval"
3031
3132
33+ # Conversation types for distinguishing between agent chats and discussion threads
34+ class ConversationTypeChoices (models .TextChoices ):
35+ CHAT = "chat" , "Chat" # Default for agent-based conversations
36+ THREAD = "thread" , "Thread" # For discussion threads
37+
38+
39+ # Agent types for multi-agent conversation support
40+ class AgentTypeChoices (models .TextChoices ):
41+ DOCUMENT_AGENT = "document_agent" , "Document Agent"
42+ CORPUS_AGENT = "corpus_agent" , "Corpus Agent"
43+
44+
45+ # Custom QuerySet for soft delete functionality
46+ class SoftDeleteQuerySet (models .QuerySet ):
47+ """
48+ QuerySet that filters soft-deleted objects and implements user visibility.
49+ """
50+
51+ def visible_to_user (self , user = None ):
52+ """
53+ Returns queryset filtered to objects visible to the user.
54+ Maintains soft-delete filtering from the base queryset.
55+ """
56+ from django .apps import apps
57+ from django .contrib .auth .models import AnonymousUser
58+ from django .db .models import Q
59+
60+ # Handle None user as anonymous
61+ if user is None :
62+ user = AnonymousUser ()
63+
64+ # Start with current queryset (already has soft-delete filtering)
65+ queryset = self
66+
67+ # Superusers see everything
68+ if hasattr (user , "is_superuser" ) and user .is_superuser :
69+ return queryset .order_by ("created" )
70+
71+ # Anonymous users only see public items
72+ if user .is_anonymous :
73+ return queryset .filter (is_public = True )
74+
75+ # Authenticated users: public, created by them, or explicitly shared
76+ model_name = self .model ._meta .model_name
77+ app_label = self .model ._meta .app_label
78+
79+ try :
80+ permission_model_name = f"{ model_name } userobjectpermission"
81+ permission_model_type = apps .get_model (app_label , permission_model_name )
82+ permitted_ids = permission_model_type .objects .filter (
83+ permission__codename = f"read_{ model_name } " , user_id = user .id
84+ ).values_list ("content_object_id" , flat = True )
85+
86+ return queryset .filter (
87+ Q (creator_id = user .id ) | Q (is_public = True ) | Q (id__in = permitted_ids )
88+ )
89+ except LookupError :
90+ # Fallback if permission model doesn't exist
91+ return queryset .filter (Q (creator_id = user .id ) | Q (is_public = True ))
92+
93+
94+ # Custom manager for soft delete functionality
95+ class SoftDeleteManager (BaseVisibilityManager ):
96+ """
97+ Manager that filters out soft-deleted objects by default and implements
98+ user visibility permissions via BaseVisibilityManager.
99+ Use Model.all_objects to access soft-deleted objects.
100+ """
101+
102+ def get_queryset (self ):
103+ # Return our custom queryset, filtered for non-deleted objects
104+ return SoftDeleteQuerySet (self .model , using = self ._db ).filter (
105+ deleted_at__isnull = True
106+ )
107+
108+
32109class ConversationUserObjectPermission (UserObjectPermissionBase ):
33110 """
34111 Permissions for Conversation objects at the user level.
@@ -73,6 +150,17 @@ class Conversation(BaseOCModel):
73150 auto_now = True ,
74151 help_text = "Timestamp when the conversation was last updated" ,
75152 )
153+ conversation_type = models .CharField (
154+ max_length = 32 ,
155+ choices = ConversationTypeChoices .choices ,
156+ default = ConversationTypeChoices .CHAT ,
157+ help_text = "Type of conversation: chat (agent-based) or thread (discussion)" ,
158+ )
159+ deleted_at = models .DateTimeField (
160+ null = True ,
161+ blank = True ,
162+ help_text = "Timestamp when the conversation was soft-deleted" ,
163+ )
76164 chat_with_corpus = models .ForeignKey (
77165 Corpus ,
78166 on_delete = models .SET_NULL ,
@@ -90,6 +178,10 @@ class Conversation(BaseOCModel):
90178 null = True ,
91179 )
92180
181+ # Managers
182+ objects = SoftDeleteManager () # Default manager excludes soft-deleted
183+ all_objects = models .Manager () # Access all objects including soft-deleted
184+
93185 class Meta :
94186 constraints = [
95187 django .db .models .CheckConstraint (
@@ -156,6 +248,21 @@ class Meta:
156248 choices = TYPE_CHOICES ,
157249 help_text = "The type of message (SYSTEM, HUMAN, or LLM)" ,
158250 )
251+ agent_type = models .CharField (
252+ max_length = 32 ,
253+ choices = AgentTypeChoices .choices ,
254+ blank = True ,
255+ null = True ,
256+ help_text = "The specific agent type that generated this message (for LLM messages)" ,
257+ )
258+ parent_message = models .ForeignKey (
259+ "self" ,
260+ on_delete = models .CASCADE ,
261+ related_name = "replies" ,
262+ blank = True ,
263+ null = True ,
264+ help_text = "Parent message for threaded replies" ,
265+ )
159266 content = models .TextField (
160267 help_text = "The textual content of the chat message" ,
161268 )
@@ -169,6 +276,11 @@ class Meta:
169276 auto_now_add = True ,
170277 help_text = "Timestamp when the chat message was created" ,
171278 )
279+ deleted_at = models .DateTimeField (
280+ null = True ,
281+ blank = True ,
282+ help_text = "Timestamp when the message was soft-deleted" ,
283+ )
172284
173285 source_document = models .ForeignKey (
174286 Document ,
@@ -198,6 +310,10 @@ class Meta:
198310 help_text = "Lifecycle state of the message for quick filtering" ,
199311 )
200312
313+ # Managers
314+ objects = SoftDeleteManager () # Default manager excludes soft-deleted
315+ all_objects = models .Manager () # Access all objects including soft-deleted
316+
201317 def __str__ (self ) -> str :
202318 return (
203319 f"ChatMessage { self .pk } - { self .msg_type } "
0 commit comments