cs-models 0.0.827__py3-none-any.whl → 0.0.847__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. cs_models/resources/CompanyOUS/models.py +2 -0
  2. cs_models/resources/CompanyOUS/schemas.py +4 -0
  3. cs_models/resources/CompanySEC/models.py +2 -0
  4. cs_models/resources/CompanySEC/schemas.py +4 -0
  5. cs_models/resources/DeepResearchAgenticUnit/__init__.py +14 -0
  6. cs_models/resources/DeepResearchAgenticUnit/models.py +123 -0
  7. cs_models/resources/DeepResearchAgenticUnit/schemas.py +50 -0
  8. cs_models/resources/DeepResearchSession/__init__.py +20 -0
  9. cs_models/resources/DeepResearchSession/models.py +170 -0
  10. cs_models/resources/DeepResearchSession/schemas.py +94 -0
  11. cs_models/resources/DeepResearchSubTask/__init__.py +20 -0
  12. cs_models/resources/DeepResearchSubTask/models.py +177 -0
  13. cs_models/resources/DeepResearchSubTask/schemas.py +105 -0
  14. cs_models/resources/MeetingUserDocument/__init__.py +0 -0
  15. cs_models/resources/MeetingUserDocument/models.py +39 -0
  16. cs_models/resources/MeetingUserDocument/schemas.py +17 -0
  17. cs_models/resources/PipelineCrawlSession/__init__.py +0 -0
  18. cs_models/resources/PipelineCrawlSession/models.py +67 -0
  19. cs_models/resources/PipelineCrawlSession/schemas.py +22 -0
  20. cs_models/resources/PipelineCrawledPage/__init__.py +0 -0
  21. cs_models/resources/PipelineCrawledPage/models.py +80 -0
  22. cs_models/resources/PipelineCrawledPage/schemas.py +34 -0
  23. cs_models/resources/PipelineDrugPortfolio/__init__.py +0 -0
  24. cs_models/resources/PipelineDrugPortfolio/models.py +92 -0
  25. cs_models/resources/PipelineDrugPortfolio/schemas.py +31 -0
  26. cs_models/resources/PipelineExtractionLog/__init__.py +0 -0
  27. cs_models/resources/PipelineExtractionLog/models.py +55 -0
  28. cs_models/resources/PipelineExtractionLog/schemas.py +23 -0
  29. cs_models/resources/PubmedMeetingSellSideSignal/__init__.py +0 -0
  30. cs_models/resources/PubmedMeetingSellSideSignal/models.py +64 -0
  31. cs_models/resources/PubmedMeetingSellSideSignal/schemas.py +21 -0
  32. cs_models/resources/PubmedMeetingUserDocument/__init__.py +0 -0
  33. cs_models/resources/PubmedMeetingUserDocument/models.py +40 -0
  34. cs_models/resources/PubmedMeetingUserDocument/schemas.py +16 -0
  35. cs_models/resources/SellSideAbstractMention/__init__.py +0 -0
  36. cs_models/resources/SellSideAbstractMention/models.py +57 -0
  37. cs_models/resources/SellSideAbstractMention/schemas.py +28 -0
  38. cs_models/resources/SellSideAbstractMentionLink/__init__.py +0 -0
  39. cs_models/resources/SellSideAbstractMentionLink/models.py +60 -0
  40. cs_models/resources/SellSideAbstractMentionLink/schemas.py +24 -0
  41. cs_models/resources/SellSideSource/__init__.py +0 -0
  42. cs_models/resources/SellSideSource/models.py +25 -0
  43. cs_models/resources/SellSideSource/schemas.py +13 -0
  44. cs_models/resources/UserDocument/models.py +7 -0
  45. cs_models/resources/UserDocument/schemas.py +2 -0
  46. cs_models/resources/UserDocumentAccess/models.py +6 -0
  47. cs_models/resources/UserDocumentAccess/schemas.py +1 -0
  48. cs_models/resources/Workbook/models.py +9 -0
  49. cs_models/resources/Workbook/schemas.py +6 -0
  50. cs_models/resources/WorkbookCommentThread/__init__.py +0 -0
  51. cs_models/resources/WorkbookCommentThread/models.py +59 -0
  52. cs_models/resources/WorkbookCommentThread/schemas.py +35 -0
  53. cs_models/resources/WorkbookThreadComment/__init__.py +0 -0
  54. cs_models/resources/WorkbookThreadComment/models.py +38 -0
  55. cs_models/resources/WorkbookThreadComment/schemas.py +14 -0
  56. {cs_models-0.0.827.dist-info → cs_models-0.0.847.dist-info}/METADATA +1 -1
  57. {cs_models-0.0.827.dist-info → cs_models-0.0.847.dist-info}/RECORD +59 -14
  58. {cs_models-0.0.827.dist-info → cs_models-0.0.847.dist-info}/WHEEL +0 -0
  59. {cs_models-0.0.827.dist-info → cs_models-0.0.847.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,55 @@
1
+ from sqlalchemy import (
2
+ Column,
3
+ Integer,
4
+ String,
5
+ DateTime,
6
+ Text,
7
+ ForeignKey,
8
+ Enum,
9
+ )
10
+ from datetime import datetime
11
+
12
+ from ...database import Base
13
+
14
+
15
+ class PipelineExtractionLogModel(Base):
16
+ __tablename__ = 'pipeline_extraction_logs'
17
+
18
+ id = Column(Integer, primary_key=True)
19
+ session_id = Column(
20
+ Integer,
21
+ ForeignKey('pipeline_crawl_sessions.id'),
22
+ nullable=False,
23
+ index=True,
24
+ )
25
+ page_id = Column(
26
+ Integer,
27
+ ForeignKey('pipeline_crawled_pages.id'),
28
+ nullable=True,
29
+ )
30
+
31
+ # Log details (MySQL ENUM)
32
+ log_level = Column(
33
+ Enum('debug', 'info', 'warning', 'error'),
34
+ nullable=False,
35
+ default='info',
36
+ index=True,
37
+ )
38
+ message = Column(Text, nullable=False)
39
+
40
+ # Context
41
+ extraction_method = Column(String(50), nullable=True)
42
+ model_name = Column(String(128), nullable=True)
43
+ tokens_used = Column(Integer, nullable=True)
44
+ api_latency_ms = Column(Integer, nullable=True)
45
+
46
+ # Error tracking
47
+ exception_type = Column(String(256), nullable=True)
48
+ exception_message = Column(Text, nullable=True)
49
+ stack_trace = Column(Text, nullable=True)
50
+
51
+ # Retry tracking
52
+ attempt_number = Column(Integer, nullable=True, default=1)
53
+ max_attempts = Column(Integer, nullable=True, default=3)
54
+
55
+ created_at = Column(DateTime, nullable=False, default=datetime.utcnow, index=True)
@@ -0,0 +1,23 @@
1
+ from marshmallow import Schema, fields
2
+
3
+
4
+ class PipelineExtractionLogSchema(Schema):
5
+ id = fields.Int(dump_only=True)
6
+ session_id = fields.Int(required=True)
7
+ page_id = fields.Int(allow_none=True)
8
+ log_level = fields.Str(required=True)
9
+ message = fields.Str(required=True)
10
+ extraction_method = fields.Str(allow_none=True)
11
+ model_name = fields.Str(allow_none=True)
12
+ tokens_used = fields.Int(allow_none=True)
13
+ api_latency_ms = fields.Int(allow_none=True)
14
+ exception_type = fields.Str(allow_none=True)
15
+ exception_message = fields.Str(allow_none=True)
16
+ stack_trace = fields.Str(allow_none=True)
17
+ attempt_number = fields.Int(allow_none=True)
18
+ max_attempts = fields.Int(allow_none=True)
19
+ created_at = fields.DateTime(dump_only=True)
20
+
21
+
22
+ class PipelineExtractionLogResourceSchema(PipelineExtractionLogSchema):
23
+ pass
@@ -0,0 +1,64 @@
1
+ from sqlalchemy import (
2
+ Column,
3
+ Integer,
4
+ Float,
5
+ DateTime,
6
+ ForeignKey,
7
+ UniqueConstraint,
8
+ )
9
+ from datetime import datetime
10
+
11
+ from ...database import Base
12
+
13
+
14
+ class PubmedMeetingSellSideSignalModel(Base):
15
+ __tablename__ = "pubmed_meeting_sell_side_signals"
16
+
17
+ id = Column(Integer, primary_key=True)
18
+
19
+ pubmed_id = Column(
20
+ Integer,
21
+ ForeignKey("pubmed.id"),
22
+ nullable=False,
23
+ index=True,
24
+ )
25
+ meeting_id = Column(
26
+ Integer,
27
+ ForeignKey("meetings.id"),
28
+ nullable=False,
29
+ index=True,
30
+ )
31
+ sell_side_source_id = Column(
32
+ Integer,
33
+ ForeignKey("sell_side_sources.id"),
34
+ nullable=False,
35
+ index=True,
36
+ )
37
+
38
+ # intensity
39
+ doc_count = Column(Integer, nullable=False) # distinct notes
40
+ mention_count = Column(Integer, nullable=False) # total mentions across those notes
41
+
42
+ # sentiment summary
43
+ avg_sentiment = Column(Float, nullable=True)
44
+ max_sentiment = Column(Float, nullable=True)
45
+
46
+ first_mention_at = Column(DateTime, nullable=True)
47
+ last_mention_at = Column(DateTime, nullable=True)
48
+
49
+ created_at = Column(DateTime, default=lambda: datetime.utcnow(), nullable=False)
50
+ updated_at = Column(
51
+ DateTime,
52
+ default=lambda: datetime.utcnow(),
53
+ onupdate=lambda: datetime.utcnow(),
54
+ nullable=False,
55
+ )
56
+
57
+ __table_args__ = (
58
+ UniqueConstraint(
59
+ "pubmed_id",
60
+ "meeting_id",
61
+ "sell_side_source_id",
62
+ name="uq_pubmed_meeting_source",
63
+ ),
64
+ )
@@ -0,0 +1,21 @@
1
+ from marshmallow import Schema, fields, validate
2
+
3
+
4
+ class PubmedMeetingSellSideSignalResourceSchema(Schema):
5
+ id = fields.Integer(dump_only=True)
6
+
7
+ pubmed_id = fields.Integer(required=True)
8
+ meeting_id = fields.Integer(required=True)
9
+ sell_side_source_id = fields.Integer(required=True)
10
+
11
+ doc_count = fields.Integer(required=True)
12
+ mention_count = fields.Integer(required=True)
13
+
14
+ avg_sentiment = fields.Float(allow_none=True)
15
+ max_sentiment = fields.Float(allow_none=True)
16
+
17
+ first_mention_at = fields.DateTime(allow_none=True)
18
+ last_mention_at = fields.DateTime(allow_none=True)
19
+
20
+ created_at = fields.DateTime(dump_only=True)
21
+ updated_at = fields.DateTime(dump_only=True)
@@ -0,0 +1,40 @@
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import (
4
+ Column,
5
+ Text,
6
+ Integer,
7
+ DateTime,
8
+ ForeignKey,
9
+ )
10
+
11
+ from ...database import Base
12
+
13
+
14
+ class PubmedMeetingUserDocumentModel(Base):
15
+ __tablename__ = "pubmed_meeting_user_documents"
16
+
17
+ id = Column(Integer, primary_key=True)
18
+ pubmed_id = Column(
19
+ Integer,
20
+ ForeignKey('pubmed.id'),
21
+ nullable=False,
22
+ )
23
+ meeting_id = Column(
24
+ Integer,
25
+ ForeignKey('meetings.id'),
26
+ nullable=False,
27
+ )
28
+ user_document_id = Column(
29
+ Integer,
30
+ ForeignKey('user_documents.id'),
31
+ nullable=False,
32
+ )
33
+ details = Column(Text, nullable=True)
34
+ updated_at = Column(
35
+ DateTime,
36
+ nullable=False,
37
+ # https://stackoverflow.com/questions/58776476/why-doesnt-freezegun-work-with-sqlalchemy-default-values
38
+ default=lambda: datetime.utcnow(),
39
+ onupdate=lambda: datetime.utcnow(),
40
+ )
@@ -0,0 +1,16 @@
1
+ from marshmallow import (
2
+ Schema,
3
+ fields,
4
+ validate,
5
+ )
6
+
7
+
8
+ class PubmedMeetingUserDocumentResourceSchema(Schema):
9
+ not_blank = validate.Length(min=1, error='Field cannot be blank')
10
+
11
+ id = fields.Integer(dump_only=True)
12
+ pubmed_id = fields.Integer(required=True)
13
+ meeting_id = fields.Integer(required=True)
14
+ user_document_id = fields.Integer(required=True)
15
+ details = fields.String(allow_none=True)
16
+ updated_at = fields.DateTime()
@@ -0,0 +1,57 @@
1
+ from sqlalchemy import (
2
+ Column,
3
+ Integer,
4
+ String,
5
+ Float,
6
+ Text,
7
+ DateTime,
8
+ ForeignKey,
9
+ )
10
+ from datetime import datetime
11
+
12
+ from ...database import Base
13
+
14
+
15
+ class SellSideAbstractMentionModel(Base):
16
+ __tablename__ = "sell_side_abstract_mentions"
17
+
18
+ id = Column(Integer, primary_key=True)
19
+
20
+ # which PDF / note
21
+ user_document_id = Column(
22
+ Integer,
23
+ ForeignKey("user_documents.id"),
24
+ nullable=False,
25
+ index=True,
26
+ )
27
+
28
+ # which conference
29
+ meeting_id = Column(
30
+ Integer,
31
+ ForeignKey("meetings.id"),
32
+ nullable=False,
33
+ index=True,
34
+ )
35
+
36
+ # optional locator
37
+ page_number = Column(Integer, nullable=True)
38
+
39
+ # raw LLM fields
40
+ title = Column(Text, nullable=True)
41
+ url = Column(Text, nullable=True)
42
+ abstract_number = Column(String(64), nullable=True)
43
+ abstract_search_query = Column(Text, nullable=True)
44
+ context = Column(Text, nullable=True)
45
+ sentiment = Column(Text, nullable=True)
46
+ llm_confidence = Column(Float, nullable=True)
47
+
48
+ raw_json = Column(Text, nullable=True)
49
+
50
+ created_at = Column(DateTime, default=lambda: datetime.utcnow(), nullable=False)
51
+ updated_at = Column(
52
+ DateTime,
53
+ default=lambda: datetime.utcnow(),
54
+ onupdate=lambda: datetime.utcnow(),
55
+ nullable=False,
56
+ )
57
+
@@ -0,0 +1,28 @@
1
+ from marshmallow import Schema, fields
2
+
3
+
4
+ class SellSideAbstractMentionResourceSchema(Schema):
5
+ id = fields.Integer(dump_only=True)
6
+
7
+ # which PDF / note
8
+ user_document_id = fields.Integer(required=True)
9
+ # which conference
10
+ meeting_id = fields.Integer(required=True)
11
+
12
+ page_number = fields.Integer(allow_none=True)
13
+ char_start = fields.Integer(allow_none=True)
14
+ char_end = fields.Integer(allow_none=True)
15
+
16
+ title = fields.String(allow_none=True)
17
+ url = fields.String(allow_none=True)
18
+ abstract_number = fields.String(allow_none=True)
19
+ abstract_search_query = fields.String(allow_none=True)
20
+ context = fields.String(allow_none=True)
21
+ sentiment = fields.String(allow_none=True)
22
+
23
+ llm_confidence = fields.Float(allow_none=True)
24
+
25
+ raw_json = fields.String(allow_none=True)
26
+
27
+ created_at = fields.DateTime(dump_only=True)
28
+ updated_at = fields.DateTime(dump_only=True)
@@ -0,0 +1,60 @@
1
+ from sqlalchemy import (
2
+ Column,
3
+ Integer,
4
+ String,
5
+ Float,
6
+ Boolean,
7
+ DateTime,
8
+ ForeignKey,
9
+ )
10
+ from datetime import datetime
11
+
12
+ from ...database import Base
13
+
14
+
15
+ class SellSideAbstractMentionLinkModel(Base):
16
+ __tablename__ = "sell_side_abstract_mention_links"
17
+
18
+ id = Column(Integer, primary_key=True)
19
+
20
+ mention_id = Column(
21
+ Integer,
22
+ ForeignKey("sell_side_abstract_mentions.id"),
23
+ nullable=False,
24
+ index=True,
25
+ )
26
+ pubmed_id = Column(
27
+ Integer,
28
+ ForeignKey("pubmed.id"),
29
+ nullable=False,
30
+ index=True,
31
+ )
32
+
33
+ # where did this candidate come from?
34
+ match_source = Column(
35
+ String(64),
36
+ nullable=False,
37
+ ) # e.g. "grid_cited", "abstract_number", "url", "title_fuzzy", "context_llm"
38
+
39
+ # overall score + feature-level scores
40
+ match_score = Column(Float, nullable=False)
41
+ number_score = Column(Float, nullable=True)
42
+ url_score = Column(Float, nullable=True)
43
+ title_score = Column(Float, nullable=True)
44
+ context_score = Column(Float, nullable=True)
45
+ llm_score = Column(Float, nullable=True)
46
+
47
+ is_primary = Column(Boolean, nullable=False, default=False)
48
+
49
+ created_at = Column(DateTime, default=lambda: datetime.utcnow(), nullable=False)
50
+ updated_at = Column(
51
+ DateTime,
52
+ default=lambda: datetime.utcnow(),
53
+ onupdate=lambda: datetime.utcnow(),
54
+ nullable=False,
55
+ )
56
+
57
+ __table_args__ = (
58
+ # you can keep multiple rows per pair (for debugging) or enforce uniqueness:
59
+ # UniqueConstraint("mention_id", "pubmed_id", name="uq_mention_pubmed"),
60
+ )
@@ -0,0 +1,24 @@
1
+ from marshmallow import Schema, fields, validate
2
+
3
+
4
+ class SellSideAbstractMentionLinkResourceSchema(Schema):
5
+ id = fields.Integer(dump_only=True)
6
+
7
+ mention_id = fields.Integer(required=True)
8
+ pubmed_id = fields.Integer(required=True)
9
+
10
+ # e.g. "grid_cited", "abstract_number", "url", "title_fuzzy", "context_llm"
11
+ match_source = fields.String(required=True, validate=validate.Length(min=1))
12
+
13
+ match_score = fields.Float(required=True)
14
+
15
+ number_score = fields.Float(allow_none=True)
16
+ url_score = fields.Float(allow_none=True)
17
+ title_score = fields.Float(allow_none=True)
18
+ context_score = fields.Float(allow_none=True)
19
+ llm_score = fields.Float(allow_none=True)
20
+
21
+ is_primary = fields.Boolean(required=True)
22
+
23
+ created_at = fields.DateTime(dump_only=True)
24
+ updated_at = fields.DateTime(dump_only=True)
File without changes
@@ -0,0 +1,25 @@
1
+ from sqlalchemy import (
2
+ Column,
3
+ Integer,
4
+ String,
5
+ DateTime,
6
+ )
7
+ from datetime import datetime
8
+
9
+ from ...database import Base
10
+
11
+
12
+ class SellSideSourceModel(Base):
13
+ __tablename__ = "sell_side_sources"
14
+
15
+ id = Column(Integer, primary_key=True)
16
+ name = Column(String(255), nullable=False, unique=True) # "Morgan Stanley"
17
+ code = Column(String(64), nullable=False, unique=True) # "MS", "JPM"
18
+
19
+ created_at = Column(DateTime, default=lambda: datetime.utcnow(), nullable=False)
20
+ updated_at = Column(
21
+ DateTime,
22
+ default=lambda: datetime.utcnow(),
23
+ onupdate=lambda: datetime.utcnow(),
24
+ nullable=False,
25
+ )
@@ -0,0 +1,13 @@
1
+ from marshmallow import Schema, fields, validate
2
+
3
+
4
+ class SellSideSourceResourceSchema(Schema):
5
+ id = fields.Integer(dump_only=True)
6
+
7
+ # e.g. "Morgan Stanley"
8
+ name = fields.String(required=True, validate=validate.Length(min=1))
9
+ # e.g. "MS", "JPM"
10
+ code = fields.String(allow_none=True)
11
+
12
+ created_at = fields.DateTime(dump_only=True)
13
+ updated_at = fields.DateTime(dump_only=True)
@@ -33,6 +33,13 @@ class UserDocumentModel(Base):
33
33
  metadata_info = Column(Text, nullable=True)
34
34
  status = Column(String(128), nullable=True)
35
35
  page_count = Column(Integer, nullable=True)
36
+ sell_side_source_id = Column(
37
+ Integer,
38
+ ForeignKey("sell_side_sources.id"),
39
+ nullable=True,
40
+ index=True,
41
+ )
42
+ sell_side_note_type = Column(String(64), nullable=True)
36
43
  updated_at = Column(
37
44
  DateTime,
38
45
  nullable=False,
@@ -21,4 +21,6 @@ class UserDocumentResourceSchema(Schema):
21
21
  category = fields.String(allow_none=True)
22
22
  status = fields.String(allow_none=True)
23
23
  page_count = fields.Integer(allow_none=True)
24
+ sell_side_source_id = fields.Integer(allow_none=True)
25
+ sell_side_note_type = fields.String(allow_none=True)
24
26
  updated_at = fields.DateTime(dump_only=True)
@@ -4,6 +4,7 @@ from sqlalchemy import (
4
4
  String,
5
5
  DateTime,
6
6
  Boolean,
7
+ ForeignKey,
7
8
  )
8
9
  from datetime import datetime
9
10
 
@@ -15,6 +16,11 @@ class UserDocumentAccessModel(Base):
15
16
 
16
17
  id = Column(Integer, primary_key=True)
17
18
  user_id = Column(String(128), nullable=False, index=True)
19
+ user_document_id = Column(
20
+ Integer,
21
+ ForeignKey('user_documents.id'),
22
+ nullable=False,
23
+ )
18
24
  provider_permission_id = Column(String(255), nullable=True)
19
25
  is_inherited = Column(Boolean, default=False)
20
26
  source_provider = Column(String(32), nullable=True)
@@ -10,6 +10,7 @@ class UserDocumentAccessResourceSchema(Schema):
10
10
 
11
11
  id = fields.Integer(dump_only=True)
12
12
  user_id = fields.String(required=True)
13
+ user_document_id = fields.Integer(required=True)
13
14
  provider_permission_id = fields.String(allow_none=True)
14
15
  is_inherited = fields.Boolean(allow_none=True)
15
16
  source_provider = fields.String(allow_none=True)
@@ -10,6 +10,7 @@ from sqlalchemy.orm import relationship
10
10
 
11
11
  from ...database import Base
12
12
  from ..WorkbookBlock.models import WorkbookBlockModel
13
+ from ..WorkbookCommentThread.models import WorkbookCommentThreadModel
13
14
 
14
15
 
15
16
  class WorkbookModel(Base):
@@ -35,3 +36,11 @@ class WorkbookModel(Base):
35
36
  order_by=WorkbookBlockModel.sequence_number,
36
37
  back_populates="workbook",
37
38
  )
39
+
40
+ comment_threads = relationship(
41
+ "WorkbookCommentThreadModel",
42
+ primaryjoin="and_(WorkbookModel.id==WorkbookCommentThreadModel.workbook_id, "
43
+ "or_(WorkbookCommentThreadModel.is_deleted==False, WorkbookCommentThreadModel.is_deleted==None))",
44
+ order_by=WorkbookCommentThreadModel.updated_at.desc(),
45
+ back_populates="workbook",
46
+ )
@@ -6,6 +6,7 @@ from marshmallow import (
6
6
  from ..WorkbookBlock.schemas import (
7
7
  WorkbookBlockResourceSchema,
8
8
  )
9
+ from ..WorkbookCommentThread.schemas import WorkbookCommentThreadResourceSchema
9
10
 
10
11
 
11
12
  class WorkbookResourceSchema(Schema):
@@ -19,6 +20,11 @@ class WorkbookResourceSchema(Schema):
19
20
  many=True,
20
21
  dump_only=True,
21
22
  )
23
+ comment_threads = fields.Nested(
24
+ WorkbookCommentThreadResourceSchema(exclude=("workbook_id",)),
25
+ many=True,
26
+ dump_only=True,
27
+ )
22
28
  is_deleted = fields.Boolean(allow_none=True)
23
29
  is_public = fields.Boolean(allow_none=True)
24
30
  is_help_center = fields.Boolean(allow_none=True)
File without changes
@@ -0,0 +1,59 @@
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import Column, DateTime, Boolean, ForeignKey, Integer, Text, String
4
+ from sqlalchemy.orm import relationship
5
+
6
+ from ...database import Base
7
+ from ..WorkbookThreadComment.models import WorkbookThreadCommentModel
8
+
9
+
10
+ class WorkbookCommentThreadModel(Base):
11
+ """
12
+ Represents a comment thread tied to a text selection in a workbook block.
13
+ """
14
+ __tablename__ = "workbook_comment_threads"
15
+
16
+ id = Column(Integer, primary_key=True)
17
+ workbook_id = Column(
18
+ Integer,
19
+ ForeignKey("workbooks.id", ondelete="CASCADE"),
20
+ nullable=False,
21
+ )
22
+ block_uid = Column(String(64), nullable=False) # Root parent block UID
23
+
24
+ # Selection context
25
+ selected_text = Column(Text, nullable=False) # The highlighted text
26
+
27
+ # Thread status
28
+ is_resolved = Column(Boolean, default=False, nullable=False)
29
+ resolved_by = Column(String(128), nullable=True) # user_id who resolved
30
+ resolved_at = Column(DateTime, nullable=True)
31
+
32
+ # Soft delete
33
+ is_deleted = Column(Boolean, default=False, nullable=True)
34
+
35
+ # Timestamps
36
+ created_at = Column(DateTime, nullable=False, default=lambda: datetime.utcnow())
37
+ updated_at = Column(
38
+ DateTime,
39
+ nullable=False,
40
+ default=lambda: datetime.utcnow(),
41
+ onupdate=lambda: datetime.utcnow(),
42
+ )
43
+ created_by = Column(String(128), nullable=False) # user_id who created thread
44
+
45
+ # Smart Grid Cells
46
+ cell_session_id = Column(Integer, nullable=True)
47
+ cell_user_query_id = Column(Integer, nullable=True)
48
+
49
+ # Relationships
50
+ workbook = relationship("WorkbookModel", back_populates="comment_threads")
51
+
52
+ comments = relationship(
53
+ "WorkbookThreadCommentModel",
54
+ primaryjoin="and_(WorkbookCommentThreadModel.id==WorkbookThreadCommentModel.thread_id, "
55
+ "or_(WorkbookThreadCommentModel.is_deleted==False, WorkbookThreadCommentModel.is_deleted==None))",
56
+ order_by=WorkbookThreadCommentModel.sequence_number,
57
+ back_populates="thread",
58
+ lazy="joined", # Eager load comments when loading thread
59
+ )
@@ -0,0 +1,35 @@
1
+ from marshmallow import Schema, fields, EXCLUDE
2
+ from ..WorkbookThreadComment.schemas import (
3
+ WorkbookThreadCommentSchema,
4
+ )
5
+
6
+
7
+ class WorkbookCommentThreadResourceSchema(Schema):
8
+ """Class for AssistantCommandResource schema"""
9
+ class Meta:
10
+ unknown = EXCLUDE
11
+
12
+ id = fields.Integer(dump_only=True, data_key="thread_id")
13
+ workbook_id = fields.Integer(required=True)
14
+ block_uid = fields.String(required=True)
15
+ selected_text = fields.String(required=True)
16
+
17
+ # Thread status
18
+ is_resolved = fields.Boolean(allow_none=True)
19
+ resolved_by = fields.String(allow_none=True)
20
+ resolved_at = fields.DateTime(allow_none=True)
21
+
22
+ # Soft delete
23
+ is_deleted = fields.Boolean(allow_none=True)
24
+
25
+ # Timestamps
26
+ created_at = fields.DateTime(dump_only=True)
27
+ updated_at = fields.DateTime(dump_only=True)
28
+ created_by = fields.String(required=True)
29
+
30
+ # Smart Grid Cells
31
+ cell_session_id = fields.Integer(allow_none=True)
32
+ cell_user_query_id = fields.Integer(allow_none=True)
33
+
34
+ # Nested comments
35
+ comments = fields.Nested(WorkbookThreadCommentSchema(exclude=("thread_id",)), many=True, dump_only=True)
File without changes
@@ -0,0 +1,38 @@
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import Column, DateTime, Boolean, ForeignKey, Integer, Text, String
4
+ from sqlalchemy.orm import relationship
5
+
6
+ from ...database import Base
7
+
8
+
9
+ class WorkbookThreadCommentModel(Base):
10
+ """
11
+ Represents an individual comment within a thread.
12
+ """
13
+ __tablename__ = "workbook_thread_comments"
14
+
15
+ id = Column(Integer, primary_key=True)
16
+ thread_id = Column(
17
+ Integer,
18
+ ForeignKey("workbook_comment_threads.id", ondelete="CASCADE"),
19
+ nullable=False,
20
+ )
21
+ user_id = Column(String(128), nullable=False)
22
+ sequence_number = Column(Integer, nullable=False) # 1 = initial comment
23
+ comment = Column(Text, nullable=False)
24
+
25
+ # Soft delete
26
+ is_deleted = Column(Boolean, default=False, nullable=True)
27
+
28
+ # Timestamps
29
+ created_at = Column(DateTime, nullable=False, default=lambda: datetime.utcnow())
30
+ updated_at = Column(
31
+ DateTime,
32
+ nullable=False,
33
+ default=lambda: datetime.utcnow(),
34
+ onupdate=lambda: datetime.utcnow(),
35
+ )
36
+
37
+ # Relationships
38
+ thread = relationship("WorkbookCommentThreadModel", back_populates="comments")