compair-core 0.3.1__py3-none-any.whl → 0.3.3__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.
Potentially problematic release.
This version of compair-core might be problematic. Click here for more details.
- compair/__init__.py +87 -0
- compair/celery_app.py +22 -0
- compair/default_groups.py +14 -0
- compair/embeddings.py +66 -0
- compair/feedback.py +79 -0
- compair/logger.py +29 -0
- compair/main.py +240 -0
- compair/models.py +355 -0
- compair/schema.py +146 -0
- compair/tasks.py +98 -0
- compair/utils.py +61 -0
- {compair_core-0.3.1.dist-info → compair_core-0.3.3.dist-info}/METADATA +5 -3
- compair_core-0.3.3.dist-info/RECORD +36 -0
- compair_core-0.3.3.dist-info/top_level.txt +3 -0
- compair_email/__init__.py +0 -0
- compair_email/email.py +6 -0
- compair_email/email_core.py +15 -0
- compair_email/templates.py +6 -0
- compair_email/templates_core.py +13 -0
- server/__init__.py +0 -0
- server/app.py +90 -0
- server/deps.py +67 -0
- server/local_model/__init__.py +1 -0
- server/local_model/app.py +62 -0
- server/providers/__init__.py +0 -0
- server/providers/console_mailer.py +9 -0
- server/providers/contracts.py +66 -0
- server/providers/local_storage.py +28 -0
- server/providers/noop_analytics.py +7 -0
- server/providers/noop_billing.py +30 -0
- server/providers/noop_ocr.py +10 -0
- server/routers/__init__.py +0 -0
- server/routers/capabilities.py +38 -0
- server/settings.py +51 -0
- compair_core-0.3.1.dist-info/RECORD +0 -5
- compair_core-0.3.1.dist-info/top_level.txt +0 -1
- {compair_core-0.3.1.dist-info → compair_core-0.3.3.dist-info}/WHEEL +0 -0
- {compair_core-0.3.1.dist-info → compair_core-0.3.3.dist-info}/licenses/LICENSE +0 -0
compair/models.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import binascii
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import secrets
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from pgvector.sqlalchemy import Vector
|
|
11
|
+
from sqlalchemy import (
|
|
12
|
+
Boolean,
|
|
13
|
+
Column,
|
|
14
|
+
DateTime,
|
|
15
|
+
ForeignKey,
|
|
16
|
+
Identity,
|
|
17
|
+
Integer,
|
|
18
|
+
String,
|
|
19
|
+
Table,
|
|
20
|
+
Text,
|
|
21
|
+
)
|
|
22
|
+
from sqlalchemy.orm import (
|
|
23
|
+
DeclarativeBase,
|
|
24
|
+
Mapped,
|
|
25
|
+
MappedAsDataclass,
|
|
26
|
+
mapped_column,
|
|
27
|
+
relationship,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Base(DeclarativeBase, MappedAsDataclass):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseObject(Base):
|
|
36
|
+
__abstract__ = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class User(Base):
|
|
40
|
+
__tablename__ = "user"
|
|
41
|
+
__table_args__ = {"schema": "public"}
|
|
42
|
+
|
|
43
|
+
user_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
|
|
44
|
+
username: Mapped[str] = mapped_column(String(128))
|
|
45
|
+
name: Mapped[str] = mapped_column(String(256))
|
|
46
|
+
role: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
|
47
|
+
profile_image: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
48
|
+
verification_token: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
49
|
+
reset_token: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
50
|
+
token_expiration: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
51
|
+
datetime_registered: Mapped[datetime]
|
|
52
|
+
status_change_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
53
|
+
password_hash: Mapped[str]
|
|
54
|
+
password_salt: Mapped[str]
|
|
55
|
+
|
|
56
|
+
status: Mapped[str] = mapped_column(String(16), default="inactive")
|
|
57
|
+
include_own_documents_in_feedback: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
58
|
+
default_publish: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
59
|
+
preferred_feedback_length: Mapped[str] = mapped_column(String(16), default="Brief")
|
|
60
|
+
hide_affiliations: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
61
|
+
|
|
62
|
+
groups = relationship("Group", secondary="user_to_group", back_populates="users")
|
|
63
|
+
documents = relationship(
|
|
64
|
+
"Document",
|
|
65
|
+
back_populates="user",
|
|
66
|
+
cascade="all, delete",
|
|
67
|
+
passive_deletes=True,
|
|
68
|
+
)
|
|
69
|
+
notes = relationship(
|
|
70
|
+
"Note",
|
|
71
|
+
back_populates="author",
|
|
72
|
+
cascade="all, delete",
|
|
73
|
+
passive_deletes=True,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
username: str,
|
|
79
|
+
name: str,
|
|
80
|
+
datetime_registered: datetime,
|
|
81
|
+
verification_token: str | None,
|
|
82
|
+
token_expiration: datetime | None,
|
|
83
|
+
):
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.username = username
|
|
86
|
+
self.name = name
|
|
87
|
+
self.datetime_registered = datetime_registered
|
|
88
|
+
self.verification_token = verification_token
|
|
89
|
+
self.token_expiration = token_expiration
|
|
90
|
+
self.status = "inactive"
|
|
91
|
+
self.status_change_date = datetime.now(timezone.utc)
|
|
92
|
+
|
|
93
|
+
def set_password(self, password: str) -> str:
|
|
94
|
+
salt = os.urandom(64)
|
|
95
|
+
self.password_salt = binascii.hexlify(salt).decode("utf-8")
|
|
96
|
+
hash_bytes = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100000)
|
|
97
|
+
self.password_hash = binascii.hexlify(hash_bytes).decode("utf-8")
|
|
98
|
+
return self.password_hash
|
|
99
|
+
|
|
100
|
+
def check_password(self, password: str) -> bool:
|
|
101
|
+
if not self.password_salt or not self.password_hash:
|
|
102
|
+
return False
|
|
103
|
+
salt = binascii.unhexlify(self.password_salt.encode("utf-8"))
|
|
104
|
+
hash_bytes = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100000)
|
|
105
|
+
hash_hex = binascii.hexlify(hash_bytes).decode("utf-8")
|
|
106
|
+
return secrets.compare_digest(self.password_hash, hash_hex)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Session(Base):
|
|
110
|
+
__tablename__ = "session"
|
|
111
|
+
__table_args__ = {"schema": "public"}
|
|
112
|
+
|
|
113
|
+
id: Mapped[str] = mapped_column(String(128), primary_key=True, init=True)
|
|
114
|
+
user_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"), index=True)
|
|
115
|
+
datetime_created: Mapped[datetime]
|
|
116
|
+
datetime_valid_until: Mapped[datetime]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class Group(BaseObject):
|
|
120
|
+
__tablename__ = "group"
|
|
121
|
+
__table_args__ = {"schema": "public"}
|
|
122
|
+
|
|
123
|
+
group_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
|
|
124
|
+
name: Mapped[str] = mapped_column(String(256))
|
|
125
|
+
datetime_created: Mapped[datetime]
|
|
126
|
+
group_image: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
127
|
+
category: Mapped[str] = mapped_column(String(256), default="Other")
|
|
128
|
+
description: Mapped[str] = mapped_column(Text, default="")
|
|
129
|
+
visibility: Mapped[str] = mapped_column(String(32), default="public")
|
|
130
|
+
|
|
131
|
+
users = relationship("User", secondary="user_to_group", back_populates="groups")
|
|
132
|
+
admins = relationship("Administrator", secondary="admin_to_group", back_populates="groups")
|
|
133
|
+
documents = relationship("Document", secondary="document_to_group", back_populates="groups")
|
|
134
|
+
notes = relationship("Note", secondary="note_to_group", back_populates="groups")
|
|
135
|
+
|
|
136
|
+
__mapper_args__ = {"primary_key": [group_id]}
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def document_count(self) -> int:
|
|
140
|
+
return len(self.documents)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def user_count(self) -> int:
|
|
144
|
+
return len(self.users)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def first_three_user_profile_images(self) -> list[str | None]:
|
|
148
|
+
return [user.profile_image for user in self.users[:3]]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Administrator(Base):
|
|
152
|
+
__tablename__ = "administrator"
|
|
153
|
+
__table_args__ = {"schema": "public"}
|
|
154
|
+
|
|
155
|
+
admin_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
|
|
156
|
+
user_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"), index=True)
|
|
157
|
+
|
|
158
|
+
user = relationship("User")
|
|
159
|
+
groups = relationship("Group", secondary="admin_to_group", back_populates="admins")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class JoinRequest(Base):
|
|
163
|
+
__tablename__ = "join_request"
|
|
164
|
+
__table_args__ = {"schema": "public"}
|
|
165
|
+
|
|
166
|
+
request_id: Mapped[int] = mapped_column(Identity(), primary_key=True, autoincrement=True, init=False)
|
|
167
|
+
user_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"))
|
|
168
|
+
group_id: Mapped[str] = mapped_column(ForeignKey("public.group.group_id", ondelete="CASCADE"))
|
|
169
|
+
datetime_requested: Mapped[datetime] = mapped_column(default=datetime.now(timezone.utc), init=False)
|
|
170
|
+
|
|
171
|
+
user = relationship("User")
|
|
172
|
+
group = relationship("Group")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class GroupInvitation(Base):
|
|
176
|
+
__tablename__ = "group_invitation"
|
|
177
|
+
__table_args__ = {"schema": "public"}
|
|
178
|
+
|
|
179
|
+
invitation_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True, autoincrement=True, init=False)
|
|
180
|
+
group_id: Mapped[str] = mapped_column(ForeignKey("public.group.group_id", ondelete="CASCADE"))
|
|
181
|
+
inviter_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"))
|
|
182
|
+
token: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
|
183
|
+
email: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
|
184
|
+
datetime_expiration: Mapped[datetime]
|
|
185
|
+
datetime_created: Mapped[datetime] = mapped_column(default=datetime.now(timezone.utc), init=False)
|
|
186
|
+
status: Mapped[str] = mapped_column(String(32), default="pending")
|
|
187
|
+
|
|
188
|
+
group = relationship("Group")
|
|
189
|
+
inviter = relationship("User")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Document(BaseObject):
|
|
193
|
+
__tablename__ = "document"
|
|
194
|
+
__table_args__ = {"schema": "public"}
|
|
195
|
+
|
|
196
|
+
document_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
|
|
197
|
+
user_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"), index=True)
|
|
198
|
+
author_id: Mapped[str]
|
|
199
|
+
title: Mapped[str]
|
|
200
|
+
content: Mapped[str] = mapped_column(Text)
|
|
201
|
+
doc_type: Mapped[str]
|
|
202
|
+
datetime_created: Mapped[datetime]
|
|
203
|
+
datetime_modified: Mapped[datetime]
|
|
204
|
+
file_key: Mapped[str | None] = mapped_column(String, nullable=True, default=None)
|
|
205
|
+
image_key: Mapped[str | None] = mapped_column(String, nullable=True, default=None)
|
|
206
|
+
is_published: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
207
|
+
embedding = mapped_column(Vector(1536))
|
|
208
|
+
|
|
209
|
+
user = relationship("User", back_populates="documents")
|
|
210
|
+
groups = relationship("Group", secondary="document_to_group", back_populates="documents")
|
|
211
|
+
chunks = relationship(
|
|
212
|
+
"Chunk",
|
|
213
|
+
back_populates="document",
|
|
214
|
+
cascade="all, delete",
|
|
215
|
+
passive_deletes=True,
|
|
216
|
+
)
|
|
217
|
+
references = relationship(
|
|
218
|
+
"Reference",
|
|
219
|
+
back_populates="document",
|
|
220
|
+
cascade="all, delete",
|
|
221
|
+
passive_deletes=True,
|
|
222
|
+
)
|
|
223
|
+
notes = relationship(
|
|
224
|
+
"Note",
|
|
225
|
+
back_populates="document",
|
|
226
|
+
cascade="all, delete",
|
|
227
|
+
passive_deletes=True,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Note(Base):
|
|
232
|
+
__tablename__ = "note"
|
|
233
|
+
__table_args__ = {"schema": "public"}
|
|
234
|
+
|
|
235
|
+
note_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
|
|
236
|
+
document_id: Mapped[str] = mapped_column(ForeignKey("public.document.document_id", ondelete="CASCADE"), index=True)
|
|
237
|
+
author_id: Mapped[str] = mapped_column(ForeignKey("public.user.user_id", ondelete="CASCADE"), index=True)
|
|
238
|
+
group_id: Mapped[str | None] = mapped_column(ForeignKey("public.group.group_id", ondelete="CASCADE"), index=True, nullable=True)
|
|
239
|
+
content: Mapped[str] = mapped_column(Text)
|
|
240
|
+
datetime_created: Mapped[datetime] = mapped_column(default=datetime.now(timezone.utc))
|
|
241
|
+
embedding = mapped_column(Vector(1536))
|
|
242
|
+
|
|
243
|
+
document = relationship("Document", back_populates="notes")
|
|
244
|
+
author = relationship("User", back_populates="notes")
|
|
245
|
+
groups = relationship("Group", back_populates="notes")
|
|
246
|
+
chunks = relationship(
|
|
247
|
+
"Chunk",
|
|
248
|
+
back_populates="note",
|
|
249
|
+
cascade="all, delete",
|
|
250
|
+
passive_deletes=True,
|
|
251
|
+
)
|
|
252
|
+
references = relationship(
|
|
253
|
+
"Reference",
|
|
254
|
+
back_populates="note",
|
|
255
|
+
cascade="all, delete",
|
|
256
|
+
passive_deletes=True,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class Chunk(Base):
|
|
261
|
+
__tablename__ = "chunk"
|
|
262
|
+
__table_args__ = {"schema": "public"}
|
|
263
|
+
|
|
264
|
+
chunk_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
|
|
265
|
+
hash: Mapped[str] = mapped_column(String(32))
|
|
266
|
+
content: Mapped[str] = mapped_column(Text)
|
|
267
|
+
document_id: Mapped[str | None] = mapped_column(ForeignKey("public.document.document_id", ondelete="CASCADE"), index=True, nullable=True)
|
|
268
|
+
note_id: Mapped[str | None] = mapped_column(ForeignKey("public.note.note_id", ondelete="CASCADE"), index=True, nullable=True)
|
|
269
|
+
chunk_type: Mapped[str] = mapped_column(String(16), default="document")
|
|
270
|
+
embedding = mapped_column(Vector(1536))
|
|
271
|
+
|
|
272
|
+
document = relationship("Document", back_populates="chunks")
|
|
273
|
+
note = relationship("Note", back_populates="chunks")
|
|
274
|
+
references = relationship(
|
|
275
|
+
"Reference",
|
|
276
|
+
back_populates="chunk",
|
|
277
|
+
cascade="all, delete",
|
|
278
|
+
passive_deletes=True,
|
|
279
|
+
)
|
|
280
|
+
feedbacks = relationship(
|
|
281
|
+
"Feedback",
|
|
282
|
+
back_populates="chunk",
|
|
283
|
+
cascade="all, delete",
|
|
284
|
+
passive_deletes=True,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class Reference(Base):
|
|
289
|
+
__tablename__ = "reference"
|
|
290
|
+
__table_args__ = {"schema": "public"}
|
|
291
|
+
|
|
292
|
+
reference_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
|
|
293
|
+
source_chunk_id: Mapped[str] = mapped_column(ForeignKey("public.chunk.chunk_id", ondelete="CASCADE"), index=True)
|
|
294
|
+
reference_document_id: Mapped[str | None] = mapped_column(ForeignKey("public.document.document_id", ondelete="CASCADE"), index=True, nullable=True)
|
|
295
|
+
reference_note_id: Mapped[str | None] = mapped_column(ForeignKey("public.note.note_id", ondelete="CASCADE"), index=True, nullable=True)
|
|
296
|
+
reference_type: Mapped[str] = mapped_column(String(16), default="document")
|
|
297
|
+
|
|
298
|
+
chunk = relationship("Chunk", back_populates="references")
|
|
299
|
+
document = relationship("Document", back_populates="references")
|
|
300
|
+
note = relationship("Note", back_populates="references")
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class Feedback(Base):
|
|
304
|
+
__tablename__ = "feedback"
|
|
305
|
+
__table_args__ = {"schema": "public"}
|
|
306
|
+
|
|
307
|
+
feedback_id: Mapped[str] = mapped_column(String(36), primary_key=True, init=False, default=lambda: str(uuid4()))
|
|
308
|
+
source_chunk_id: Mapped[str] = mapped_column(ForeignKey("public.chunk.chunk_id", ondelete="CASCADE"), index=True)
|
|
309
|
+
feedback: Mapped[str] = mapped_column(Text)
|
|
310
|
+
model: Mapped[str] = mapped_column(Text)
|
|
311
|
+
timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=datetime.now(timezone.utc))
|
|
312
|
+
user_feedback: Mapped[str | None] = mapped_column(String(16), nullable=True, default=None)
|
|
313
|
+
is_hidden: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
314
|
+
|
|
315
|
+
chunk = relationship("Chunk", back_populates="feedbacks")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
user_to_group_table = Table(
|
|
319
|
+
"user_to_group",
|
|
320
|
+
Base.metadata,
|
|
321
|
+
Column("user_id", ForeignKey("public.user.user_id", ondelete="CASCADE"), primary_key=True),
|
|
322
|
+
Column("group_id", ForeignKey("public.group.group_id", ondelete="CASCADE"), primary_key=True),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
admin_to_group_table = Table(
|
|
327
|
+
"admin_to_group",
|
|
328
|
+
Base.metadata,
|
|
329
|
+
Column("admin_id", ForeignKey("public.administrator.admin_id", ondelete="CASCADE"), primary_key=True),
|
|
330
|
+
Column("group_id", ForeignKey("public.group.group_id", ondelete="CASCADE"), primary_key=True),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
document_to_group_table = Table(
|
|
335
|
+
"document_to_group",
|
|
336
|
+
Base.metadata,
|
|
337
|
+
Column("document_id", ForeignKey("public.document.document_id", ondelete="CASCADE"), primary_key=True),
|
|
338
|
+
Column("group_id", ForeignKey("public.group.group_id", ondelete="CASCADE"), primary_key=True),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
note_to_group_table = Table(
|
|
342
|
+
"note_to_group",
|
|
343
|
+
Base.metadata,
|
|
344
|
+
Column("note_id", ForeignKey("public.note.note_id", ondelete="CASCADE"), primary_key=True),
|
|
345
|
+
Column("group_id", ForeignKey("public.group.group_id", ondelete="CASCADE"), primary_key=True),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
from compair_cloud.models import extend_models # type: ignore
|
|
351
|
+
except (ImportError, ModuleNotFoundError):
|
|
352
|
+
extend_models = None
|
|
353
|
+
|
|
354
|
+
if extend_models:
|
|
355
|
+
extend_models(Base, globals())
|
compair/schema.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GroupForm:
|
|
10
|
+
name: str
|
|
11
|
+
user_id: Optional[str] = None
|
|
12
|
+
group_id: Optional[str] = None
|
|
13
|
+
datetime_created: Optional[datetime] = None
|
|
14
|
+
category: Optional[str] = None
|
|
15
|
+
description: Optional[str] = None
|
|
16
|
+
visibility: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Group(BaseModel):
|
|
20
|
+
name: str
|
|
21
|
+
user_id: Optional[str] = None
|
|
22
|
+
group_id: Optional[str] = None
|
|
23
|
+
datetime_created: Optional[datetime] = None
|
|
24
|
+
group_image: Optional[str] = None
|
|
25
|
+
category: Optional[str] = None
|
|
26
|
+
description: Optional[str] = None
|
|
27
|
+
visibility: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
model_config = {"from_attributes": True}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class User(BaseModel):
|
|
33
|
+
user_id: str
|
|
34
|
+
username: str
|
|
35
|
+
name: str
|
|
36
|
+
datetime_registered: datetime
|
|
37
|
+
status: str
|
|
38
|
+
groups: Optional[list[Group]] = None
|
|
39
|
+
profile_image: Optional[str] = None
|
|
40
|
+
role: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
model_config = {"from_attributes": True}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class UpdateUserRequest(BaseModel):
|
|
46
|
+
user_id: str
|
|
47
|
+
name: Optional[str] = None
|
|
48
|
+
role: Optional[str] = None
|
|
49
|
+
group_ids: Optional[list[str]] = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Session(BaseModel):
|
|
53
|
+
id: str
|
|
54
|
+
user_id: str
|
|
55
|
+
datetime_created: datetime
|
|
56
|
+
datetime_valid_until: datetime
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Document(BaseModel):
|
|
60
|
+
document_id: str
|
|
61
|
+
user_id: str
|
|
62
|
+
author_id: str
|
|
63
|
+
groups: list[Group]
|
|
64
|
+
user: User
|
|
65
|
+
title: str
|
|
66
|
+
content: str
|
|
67
|
+
doc_type: str
|
|
68
|
+
datetime_created: datetime
|
|
69
|
+
datetime_modified: datetime
|
|
70
|
+
is_published: bool
|
|
71
|
+
file_key: Optional[str] = None
|
|
72
|
+
image_key: Optional[str] = None
|
|
73
|
+
|
|
74
|
+
model_config = {"from_attributes": True}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Chunk(BaseModel):
|
|
78
|
+
chunk_id: str
|
|
79
|
+
hash: str
|
|
80
|
+
document_id: str
|
|
81
|
+
content: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Reference(BaseModel):
|
|
85
|
+
reference_id: str
|
|
86
|
+
source_chunk_id: str
|
|
87
|
+
reference_document_id: str
|
|
88
|
+
document: Document
|
|
89
|
+
document_author: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Feedback(BaseModel):
|
|
93
|
+
feedback_id: str
|
|
94
|
+
source_chunk_id: str
|
|
95
|
+
feedback: str
|
|
96
|
+
user_feedback: str | None = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class LoginRequest(BaseModel):
|
|
100
|
+
username: str
|
|
101
|
+
password: str
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SignUpRequest(BaseModel):
|
|
105
|
+
username: str
|
|
106
|
+
name: str
|
|
107
|
+
password: str
|
|
108
|
+
groups: list[Group] | None
|
|
109
|
+
referral_code: str | None = None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ForgotPasswordRequest(BaseModel):
|
|
113
|
+
email: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ResetPasswordRequest(BaseModel):
|
|
117
|
+
token: str
|
|
118
|
+
new_password: str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Note(BaseModel):
|
|
122
|
+
note_id: str
|
|
123
|
+
document_id: str
|
|
124
|
+
author_id: str
|
|
125
|
+
group_id: str | None = None
|
|
126
|
+
content: str
|
|
127
|
+
datetime_created: datetime
|
|
128
|
+
author: User | None = None
|
|
129
|
+
group: Group | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class InviteToGroupRequest(BaseModel):
|
|
133
|
+
admin_id: str
|
|
134
|
+
group_id: str
|
|
135
|
+
email: str
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class InviteMemberRequest(BaseModel):
|
|
139
|
+
admin_id: str
|
|
140
|
+
group_id: str
|
|
141
|
+
username: str
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class RemoveMemberRequest(BaseModel):
|
|
145
|
+
group_id: str
|
|
146
|
+
user_id: str
|
compair/tasks.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from compair_cloud.tasks import ( # type: ignore
|
|
8
|
+
process_document_task,
|
|
9
|
+
process_text_task,
|
|
10
|
+
check_trial_expirations,
|
|
11
|
+
expire_group_invitations,
|
|
12
|
+
send_trial_warnings,
|
|
13
|
+
send_feature_announcement_task,
|
|
14
|
+
send_deactivate_request_email,
|
|
15
|
+
send_help_request_email,
|
|
16
|
+
send_daily_usage_report,
|
|
17
|
+
)
|
|
18
|
+
except (ImportError, ModuleNotFoundError):
|
|
19
|
+
from sqlalchemy.orm import joinedload
|
|
20
|
+
|
|
21
|
+
def _lazy_components():
|
|
22
|
+
from . import Session as SessionMaker
|
|
23
|
+
from .embeddings import Embedder
|
|
24
|
+
from .feedback import Reviewer
|
|
25
|
+
from .logger import log_event
|
|
26
|
+
from .main import process_document
|
|
27
|
+
from .models import Document, User
|
|
28
|
+
|
|
29
|
+
return SessionMaker, Embedder, Reviewer, log_event, process_document, Document, User
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
def process_document_task(
|
|
34
|
+
user_id: str,
|
|
35
|
+
doc_id: str,
|
|
36
|
+
doc_text: str,
|
|
37
|
+
generate_feedback: bool = True,
|
|
38
|
+
) -> Mapping[str, list[str]]:
|
|
39
|
+
SessionMaker, Embedder, Reviewer, log_event, process_document, Document, User = _lazy_components()
|
|
40
|
+
with SessionMaker() as session:
|
|
41
|
+
user = session.query(User).filter(User.user_id == user_id).first()
|
|
42
|
+
if not user:
|
|
43
|
+
logger.warning("User not found for document processing", extra={"user_id": user_id})
|
|
44
|
+
return {"chunk_task_ids": []}
|
|
45
|
+
|
|
46
|
+
doc = (
|
|
47
|
+
session.query(Document)
|
|
48
|
+
.options(joinedload(Document.groups))
|
|
49
|
+
.filter(Document.document_id == doc_id)
|
|
50
|
+
.first()
|
|
51
|
+
)
|
|
52
|
+
if not doc:
|
|
53
|
+
logger.warning("Document not found for processing", extra={"document_id": doc_id})
|
|
54
|
+
return {"chunk_task_ids": []}
|
|
55
|
+
|
|
56
|
+
doc.content = doc_text
|
|
57
|
+
session.add(doc)
|
|
58
|
+
|
|
59
|
+
embedder = Embedder()
|
|
60
|
+
reviewer = Reviewer()
|
|
61
|
+
|
|
62
|
+
process_document(user, session, embedder, reviewer, doc, generate_feedback=generate_feedback)
|
|
63
|
+
|
|
64
|
+
log_event(
|
|
65
|
+
"core_document_processed",
|
|
66
|
+
user_id=user_id,
|
|
67
|
+
document_id=doc_id,
|
|
68
|
+
feedback_requested=generate_feedback,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return {"chunk_task_ids": []}
|
|
72
|
+
|
|
73
|
+
def process_text_task(*args, **kwargs): # pragma: no cover
|
|
74
|
+
raise RuntimeError("process_text_task is only available in the Compair Cloud edition.")
|
|
75
|
+
|
|
76
|
+
def check_trial_expirations(): # pragma: no cover
|
|
77
|
+
raise RuntimeError("check_trial_expirations is only available in the Compair Cloud edition.")
|
|
78
|
+
|
|
79
|
+
def expire_group_invitations(): # pragma: no cover
|
|
80
|
+
raise RuntimeError("expire_group_invitations is only available in the Compair Cloud edition.")
|
|
81
|
+
|
|
82
|
+
def send_trial_warnings(): # pragma: no cover
|
|
83
|
+
raise RuntimeError("send_trial_warnings is only available in the Compair Cloud edition.")
|
|
84
|
+
|
|
85
|
+
def send_feature_announcement_task(): # pragma: no cover
|
|
86
|
+
raise RuntimeError("send_feature_announcement_task is only available in the Compair Cloud edition.")
|
|
87
|
+
|
|
88
|
+
def send_deactivate_request_email(*args, **kwargs): # pragma: no cover
|
|
89
|
+
raise RuntimeError("send_deactivate_request_email is only available in the Compair Cloud edition.")
|
|
90
|
+
|
|
91
|
+
def send_help_request_email(*args, **kwargs): # pragma: no cover
|
|
92
|
+
raise RuntimeError("send_help_request_email is only available in the Compair Cloud edition.")
|
|
93
|
+
|
|
94
|
+
def send_daily_usage_report(): # pragma: no cover
|
|
95
|
+
raise RuntimeError("send_daily_usage_report is only available in the Compair Cloud edition.")
|
|
96
|
+
|
|
97
|
+
def process_file_with_ocr_task(*args, **kwargs): # pragma: no cover
|
|
98
|
+
raise RuntimeError("OCR processing is only available in the Compair Cloud edition.")
|
compair/utils.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.orm import Session
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from compair_cloud.utils import log_activity as cloud_log_activity # type: ignore
|
|
11
|
+
except (ImportError, ModuleNotFoundError):
|
|
12
|
+
cloud_log_activity = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def chunk_text(text: str) -> list[str]:
|
|
16
|
+
chunks = text.split("\n\n")
|
|
17
|
+
chunks = [c.strip() for c in chunks]
|
|
18
|
+
return [c for c in chunks if c]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_verification_token() -> tuple[str, datetime]:
|
|
22
|
+
token = secrets.token_urlsafe(32)
|
|
23
|
+
expiration = datetime.now(timezone.utc) + timedelta(hours=24)
|
|
24
|
+
return token, expiration
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def log_activity(
|
|
28
|
+
session: Session,
|
|
29
|
+
user_id: str,
|
|
30
|
+
group_id: str,
|
|
31
|
+
action: str,
|
|
32
|
+
object_id: str,
|
|
33
|
+
object_name: str,
|
|
34
|
+
object_type: str,
|
|
35
|
+
) -> None:
|
|
36
|
+
if cloud_log_activity:
|
|
37
|
+
cloud_log_activity(
|
|
38
|
+
session=session,
|
|
39
|
+
user_id=user_id,
|
|
40
|
+
group_id=group_id,
|
|
41
|
+
action=action,
|
|
42
|
+
object_id=object_id,
|
|
43
|
+
object_name=object_name,
|
|
44
|
+
object_type=object_type,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def aggregate_usage_by_user() -> dict[str, Any]:
|
|
49
|
+
if cloud_log_activity:
|
|
50
|
+
from compair_cloud.utils import aggregate_usage_by_user as cloud_usage # type: ignore
|
|
51
|
+
|
|
52
|
+
return cloud_usage()
|
|
53
|
+
return {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def aggregate_service_resources() -> dict[str, Any]:
|
|
57
|
+
if cloud_log_activity:
|
|
58
|
+
from compair_cloud.utils import aggregate_service_resources as cloud_resources # type: ignore
|
|
59
|
+
|
|
60
|
+
return cloud_resources()
|
|
61
|
+
return {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compair-core
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Open-source foundation of the Compair collaboration platform.
|
|
5
5
|
Author: RocketResearch, Inc.
|
|
6
6
|
License: MIT
|
|
@@ -96,8 +96,8 @@ See `compair_core/server/settings.py` for the full settings surface.
|
|
|
96
96
|
```bash
|
|
97
97
|
python -m venv .venv
|
|
98
98
|
source .venv/bin/activate
|
|
99
|
-
pip install -e .[dev]
|
|
100
|
-
uvicorn
|
|
99
|
+
pip install -e ".[dev]"
|
|
100
|
+
uvicorn compair.server.app:create_app --factory --reload
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
The API will be available at http://127.0.0.1:8000 and supports the Swagger UI at `/docs`.
|
|
@@ -106,6 +106,8 @@ The API will be available at http://127.0.0.1:8000 and supports the Swagger UI a
|
|
|
106
106
|
|
|
107
107
|
Core currently ships with a syntax sanity check (`python -m compileall ...`). You can add pytest or other tooling as needed.
|
|
108
108
|
|
|
109
|
+
Release and packaging steps are documented in `docs/maintainers.md`.
|
|
110
|
+
|
|
109
111
|
## Reporting Issues
|
|
110
112
|
|
|
111
113
|
Please open GitHub issues or PRs against this repository. If you are a Compair Cloud customer, reach out through your support channel for issues related to premium features.
|