constec 0.7.1__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.
- constec/db/__init__.py +11 -0
- constec/db/apps.py +8 -0
- constec/db/migrations/0001_initial.py +551 -0
- constec/db/migrations/0002_module_level.py +38 -0
- constec/db/migrations/0003_remove_module_level.py +15 -0
- constec/db/migrations/0004_rename_entities_company_cuit_idx_entities_company_e2c50f_idx_and_more.py +345 -0
- constec/db/migrations/0005_event.py +46 -0
- constec/db/migrations/0006_automation_trigger_action_executionlog_notificationtemplate.py +275 -0
- constec/db/migrations/0007_add_organization_to_automations.py +91 -0
- constec/db/migrations/0008_refactor_creator_fields.py +173 -0
- constec/db/migrations/0009_rename_user_to_companyuser.py +40 -0
- constec/db/migrations/__init__.py +0 -0
- constec/db/models/__init__.py +110 -0
- constec/db/models/automation.py +488 -0
- constec/db/models/base.py +23 -0
- constec/db/models/company.py +36 -0
- constec/db/models/contact.py +71 -0
- constec/db/models/erp.py +101 -0
- constec/db/models/erp_entity.py +122 -0
- constec/db/models/flow.py +138 -0
- constec/db/models/group.py +36 -0
- constec/db/models/module.py +67 -0
- constec/db/models/organization.py +62 -0
- constec/db/models/person.py +28 -0
- constec/db/models/session.py +89 -0
- constec/db/models/tag.py +70 -0
- constec/db/models/user.py +74 -0
- constec/py.typed +0 -0
- constec/services/__init__.py +14 -0
- constec/services/encryption.py +92 -0
- constec/shared/__init__.py +20 -0
- constec/shared/exceptions.py +48 -0
- constec/utils/__init__.py +20 -0
- constec/utils/cuit.py +107 -0
- constec/utils/password.py +62 -0
- constec-0.7.1.dist-info/METADATA +94 -0
- constec-0.7.1.dist-info/RECORD +40 -0
- constec-0.7.1.dist-info/WHEEL +5 -0
- constec-0.7.1.dist-info/licenses/LICENSE +21 -0
- constec-0.7.1.dist-info/top_level.txt +1 -0
constec/db/models/erp.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .company import Company
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class System(UUIDModel):
|
|
7
|
+
"""ERP system definition - independent of any company.
|
|
8
|
+
|
|
9
|
+
An ERP system can be shared across multiple companies via CompanySystem.
|
|
10
|
+
"""
|
|
11
|
+
name = models.CharField(max_length=100, unique=True)
|
|
12
|
+
description = models.TextField(blank=True, null=True)
|
|
13
|
+
api_endpoint = models.URLField(max_length=200, blank=True)
|
|
14
|
+
metadata = models.JSONField(default=dict, blank=True)
|
|
15
|
+
|
|
16
|
+
class Meta:
|
|
17
|
+
app_label = 'constec_db'
|
|
18
|
+
db_table = 'erp"."systems'
|
|
19
|
+
|
|
20
|
+
def __str__(self):
|
|
21
|
+
return f"{self.name}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CompanySystem(UUIDModel):
|
|
25
|
+
"""Junction table linking companies to ERP systems.
|
|
26
|
+
|
|
27
|
+
Allows many-to-many relationship:
|
|
28
|
+
- A company can use multiple ERP systems
|
|
29
|
+
- An ERP system can be used by multiple companies
|
|
30
|
+
"""
|
|
31
|
+
company = models.ForeignKey(
|
|
32
|
+
Company,
|
|
33
|
+
on_delete=models.CASCADE,
|
|
34
|
+
related_name="erp_systems",
|
|
35
|
+
)
|
|
36
|
+
system = models.ForeignKey(
|
|
37
|
+
System,
|
|
38
|
+
on_delete=models.CASCADE,
|
|
39
|
+
related_name="company_assignments",
|
|
40
|
+
)
|
|
41
|
+
is_primary = models.BooleanField(
|
|
42
|
+
default=False,
|
|
43
|
+
help_text="Whether this is the primary ERP system for the company"
|
|
44
|
+
)
|
|
45
|
+
metadata = models.JSONField(default=dict, blank=True)
|
|
46
|
+
|
|
47
|
+
class Meta:
|
|
48
|
+
app_label = 'constec_db'
|
|
49
|
+
db_table = 'erp"."company_systems'
|
|
50
|
+
unique_together = [['company', 'system']]
|
|
51
|
+
indexes = [
|
|
52
|
+
models.Index(fields=['company', 'is_primary']),
|
|
53
|
+
models.Index(fields=['system', 'is_active']),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def __str__(self):
|
|
57
|
+
primary = " (Primary)" if self.is_primary else ""
|
|
58
|
+
return f"{self.company.name} → {self.system.name}{primary}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Connection(UUIDModel):
|
|
62
|
+
"""Database connection credentials for an ERP system."""
|
|
63
|
+
|
|
64
|
+
DB_TYPE_CHOICES = [
|
|
65
|
+
('mssql', 'SQL Server'),
|
|
66
|
+
('postgresql', 'PostgreSQL'),
|
|
67
|
+
('mysql', 'MySQL'),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
company = models.ForeignKey(
|
|
71
|
+
Company,
|
|
72
|
+
on_delete=models.CASCADE,
|
|
73
|
+
related_name="erp_connections",
|
|
74
|
+
)
|
|
75
|
+
system = models.ForeignKey(
|
|
76
|
+
System,
|
|
77
|
+
on_delete=models.CASCADE,
|
|
78
|
+
related_name="connections",
|
|
79
|
+
)
|
|
80
|
+
name = models.CharField(
|
|
81
|
+
max_length=100,
|
|
82
|
+
blank=True,
|
|
83
|
+
default='',
|
|
84
|
+
help_text="Descriptive name for this connection"
|
|
85
|
+
)
|
|
86
|
+
slug = models.CharField(max_length=50)
|
|
87
|
+
db_type = models.CharField(max_length=20, choices=DB_TYPE_CHOICES, default='mssql')
|
|
88
|
+
host = models.CharField(max_length=255)
|
|
89
|
+
port = models.IntegerField()
|
|
90
|
+
database = models.CharField(max_length=100)
|
|
91
|
+
username = models.CharField(max_length=100)
|
|
92
|
+
encrypted_password = models.TextField()
|
|
93
|
+
last_tested_at = models.DateTimeField(null=True, blank=True)
|
|
94
|
+
|
|
95
|
+
class Meta:
|
|
96
|
+
app_label = 'constec_db'
|
|
97
|
+
db_table = 'erp"."connections'
|
|
98
|
+
unique_together = [['system', 'slug']]
|
|
99
|
+
|
|
100
|
+
def __str__(self):
|
|
101
|
+
return f"{self.name}" if self.name else f"{self.system.name} ({self.slug})"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from django.core.exceptions import ValidationError
|
|
2
|
+
from django.db import models
|
|
3
|
+
from .base import UUIDModel
|
|
4
|
+
from .company import Company
|
|
5
|
+
from .erp import System
|
|
6
|
+
from .person import Person
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Role(UUIDModel):
|
|
10
|
+
"""Roles within the ERP (customer, supplier, seller)."""
|
|
11
|
+
system = models.ForeignKey(
|
|
12
|
+
System,
|
|
13
|
+
on_delete=models.CASCADE,
|
|
14
|
+
related_name="roles",
|
|
15
|
+
)
|
|
16
|
+
name = models.CharField(max_length=100)
|
|
17
|
+
description = models.TextField(blank=True, null=True)
|
|
18
|
+
|
|
19
|
+
class Meta:
|
|
20
|
+
app_label = 'constec_db'
|
|
21
|
+
db_table = 'erp"."roles'
|
|
22
|
+
unique_together = [['system', 'name']]
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return f"{self.name}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Entity(UUIDModel):
|
|
29
|
+
"""Link between ERP external entities and Core persons.
|
|
30
|
+
|
|
31
|
+
An Entity can exist without a Person (when someone has ERP access
|
|
32
|
+
but no Core account yet). When they authenticate via CUIT, we can
|
|
33
|
+
optionally link them to a Person later.
|
|
34
|
+
"""
|
|
35
|
+
company = models.ForeignKey(
|
|
36
|
+
Company,
|
|
37
|
+
on_delete=models.CASCADE,
|
|
38
|
+
related_name="erp_entities",
|
|
39
|
+
)
|
|
40
|
+
person = models.ForeignKey(
|
|
41
|
+
Person,
|
|
42
|
+
on_delete=models.CASCADE,
|
|
43
|
+
related_name="erp_entities",
|
|
44
|
+
null=True,
|
|
45
|
+
blank=True,
|
|
46
|
+
help_text="Optional link to Core person"
|
|
47
|
+
)
|
|
48
|
+
system = models.ForeignKey(
|
|
49
|
+
System,
|
|
50
|
+
on_delete=models.CASCADE,
|
|
51
|
+
related_name="entities",
|
|
52
|
+
)
|
|
53
|
+
role = models.ForeignKey(
|
|
54
|
+
Role,
|
|
55
|
+
on_delete=models.PROTECT,
|
|
56
|
+
related_name="entities",
|
|
57
|
+
)
|
|
58
|
+
external_id = models.CharField(
|
|
59
|
+
max_length=255,
|
|
60
|
+
help_text="Entity ID in external ERP system (e.g., cli_Cod, pro_Cod)"
|
|
61
|
+
)
|
|
62
|
+
cuit = models.CharField(
|
|
63
|
+
max_length=13,
|
|
64
|
+
blank=True,
|
|
65
|
+
default='',
|
|
66
|
+
help_text="CUIT without dashes (for authentication lookup)"
|
|
67
|
+
)
|
|
68
|
+
metadata = models.JSONField(default=dict, blank=True)
|
|
69
|
+
|
|
70
|
+
class Meta:
|
|
71
|
+
app_label = 'constec_db'
|
|
72
|
+
db_table = 'erp"."entities'
|
|
73
|
+
verbose_name_plural = 'Entities'
|
|
74
|
+
unique_together = [['company', 'role', 'external_id']]
|
|
75
|
+
indexes = [
|
|
76
|
+
models.Index(fields=['cuit']),
|
|
77
|
+
models.Index(fields=['company', 'cuit']),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def clean(self):
|
|
81
|
+
from .erp import CompanySystem
|
|
82
|
+
if not CompanySystem.objects.filter(company=self.company, system=self.system).exists():
|
|
83
|
+
raise ValidationError(
|
|
84
|
+
f"Company {self.company} is not associated with System {self.system}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def __str__(self):
|
|
88
|
+
person_info = f"({self.person.full_name})" if self.person else "(no person linked)"
|
|
89
|
+
return f"[{self.system.name}] {self.role.name} {self.external_id} {person_info}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class EntityAuth(UUIDModel):
|
|
93
|
+
"""Authentication credentials for ERP entities.
|
|
94
|
+
|
|
95
|
+
Stores hashed passwords for entities that can authenticate via Constancia AI.
|
|
96
|
+
Linked to Entity (which may or may not have a Person linked).
|
|
97
|
+
"""
|
|
98
|
+
entity = models.OneToOneField(
|
|
99
|
+
Entity,
|
|
100
|
+
on_delete=models.CASCADE,
|
|
101
|
+
related_name="auth",
|
|
102
|
+
help_text="The ERP entity these credentials belong to"
|
|
103
|
+
)
|
|
104
|
+
password_hash = models.CharField(
|
|
105
|
+
max_length=255,
|
|
106
|
+
help_text="Bcrypt hashed password"
|
|
107
|
+
)
|
|
108
|
+
last_login = models.DateTimeField(
|
|
109
|
+
null=True,
|
|
110
|
+
blank=True,
|
|
111
|
+
help_text="Last successful login timestamp"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
class Meta:
|
|
115
|
+
app_label = 'constec_db'
|
|
116
|
+
db_table = 'erp"."entity_auth'
|
|
117
|
+
verbose_name = "Entity Authentication"
|
|
118
|
+
verbose_name_plural = "Entity Authentications"
|
|
119
|
+
|
|
120
|
+
def __str__(self):
|
|
121
|
+
status = "active" if self.is_active else "inactive"
|
|
122
|
+
return f"Auth for {self.entity} ({status})"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .organization import Organization, OrganizationUser
|
|
4
|
+
from .company import Company
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FlowTemplate(UUIDModel):
|
|
8
|
+
"""Global flow templates available to all companies.
|
|
9
|
+
|
|
10
|
+
Created by organization admins. Companies can use these
|
|
11
|
+
templates as-is or customize them.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
organization = models.ForeignKey(
|
|
15
|
+
Organization,
|
|
16
|
+
on_delete=models.CASCADE,
|
|
17
|
+
related_name="flow_templates",
|
|
18
|
+
null=True,
|
|
19
|
+
blank=True,
|
|
20
|
+
help_text="Organization that owns this template (null for global)"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
name = models.CharField(max_length=100)
|
|
24
|
+
description = models.TextField()
|
|
25
|
+
category = models.CharField(
|
|
26
|
+
max_length=50,
|
|
27
|
+
choices=[
|
|
28
|
+
('finance', 'Finance & Accounting'),
|
|
29
|
+
('customer_service', 'Customer Service'),
|
|
30
|
+
('sales', 'Sales & CRM'),
|
|
31
|
+
('general', 'General Purpose'),
|
|
32
|
+
('custom', 'Custom'),
|
|
33
|
+
]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
graph_definition = models.JSONField(
|
|
37
|
+
help_text="Graph structure in JSON format with nodes and edges"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
default_config = models.JSONField(
|
|
41
|
+
default=dict,
|
|
42
|
+
help_text="Default configuration for this template (LLM, tools, prompts)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
is_global = models.BooleanField(
|
|
46
|
+
default=False,
|
|
47
|
+
help_text="If true, all organizations can use this template"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
version = models.CharField(max_length=20, default="1.0.0")
|
|
51
|
+
created_by = models.ForeignKey(
|
|
52
|
+
OrganizationUser,
|
|
53
|
+
on_delete=models.SET_NULL,
|
|
54
|
+
null=True,
|
|
55
|
+
related_name="created_templates"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
class Meta:
|
|
59
|
+
app_label = 'constec_db'
|
|
60
|
+
db_table = 'constancia"."flow_templates'
|
|
61
|
+
unique_together = [['organization', 'name', 'version']]
|
|
62
|
+
indexes = [
|
|
63
|
+
models.Index(fields=['organization', 'is_global']),
|
|
64
|
+
models.Index(fields=['category', 'is_active']),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
def __str__(self):
|
|
68
|
+
global_tag = " [GLOBAL]" if self.is_global else ""
|
|
69
|
+
return f"{self.name} v{self.version}{global_tag}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Flow(UUIDModel):
|
|
73
|
+
"""Company-specific flow configuration.
|
|
74
|
+
|
|
75
|
+
Can either use a template or define custom graph.
|
|
76
|
+
Defines how the AI agent behaves for a specific company.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
company = models.ForeignKey(
|
|
80
|
+
Company,
|
|
81
|
+
on_delete=models.CASCADE,
|
|
82
|
+
related_name="flows",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
name = models.CharField(max_length=100)
|
|
86
|
+
description = models.TextField(blank=True, null=True)
|
|
87
|
+
|
|
88
|
+
template = models.ForeignKey(
|
|
89
|
+
FlowTemplate,
|
|
90
|
+
null=True,
|
|
91
|
+
blank=True,
|
|
92
|
+
on_delete=models.SET_NULL,
|
|
93
|
+
related_name="flows",
|
|
94
|
+
help_text="If set, uses template's graph. config overrides template defaults."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
graph_definition = models.JSONField(
|
|
98
|
+
null=True,
|
|
99
|
+
blank=True,
|
|
100
|
+
help_text="Custom graph structure (only if not using template)"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
config = models.JSONField(
|
|
104
|
+
default=dict,
|
|
105
|
+
help_text="Configuration that merges with template.default_config"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
is_default = models.BooleanField(
|
|
109
|
+
default=False,
|
|
110
|
+
help_text="Whether this is the default flow for the company"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
class Meta:
|
|
114
|
+
app_label = 'constec_db'
|
|
115
|
+
db_table = 'constancia"."flows'
|
|
116
|
+
unique_together = [['company', 'name']]
|
|
117
|
+
indexes = [
|
|
118
|
+
models.Index(fields=['company', 'is_default']),
|
|
119
|
+
models.Index(fields=['template', 'is_active']),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def get_graph_definition(self):
|
|
123
|
+
"""Returns the graph definition (from template or custom)."""
|
|
124
|
+
if self.template:
|
|
125
|
+
return self.template.graph_definition
|
|
126
|
+
return self.graph_definition
|
|
127
|
+
|
|
128
|
+
def get_merged_config(self):
|
|
129
|
+
"""Merges template defaults with flow config."""
|
|
130
|
+
if self.template:
|
|
131
|
+
merged = {**self.template.default_config}
|
|
132
|
+
merged.update(self.config)
|
|
133
|
+
return merged
|
|
134
|
+
return self.config
|
|
135
|
+
|
|
136
|
+
def __str__(self):
|
|
137
|
+
default = " (Default)" if self.is_default else ""
|
|
138
|
+
return f"{self.company.name} - {self.name}{default}"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .company import Company
|
|
4
|
+
from .user import CompanyUser
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UserGroup(UUIDModel):
|
|
8
|
+
"""Hierarchical user groups within a company."""
|
|
9
|
+
company = models.ForeignKey(
|
|
10
|
+
Company,
|
|
11
|
+
on_delete=models.CASCADE,
|
|
12
|
+
related_name="user_groups",
|
|
13
|
+
null=True,
|
|
14
|
+
blank=True
|
|
15
|
+
)
|
|
16
|
+
name = models.CharField(max_length=100)
|
|
17
|
+
description = models.TextField(blank=True, null=True)
|
|
18
|
+
parent = models.ForeignKey(
|
|
19
|
+
'self',
|
|
20
|
+
on_delete=models.CASCADE,
|
|
21
|
+
related_name="sub_groups",
|
|
22
|
+
null=True,
|
|
23
|
+
blank=True
|
|
24
|
+
)
|
|
25
|
+
users = models.ManyToManyField(
|
|
26
|
+
CompanyUser,
|
|
27
|
+
related_name="groups"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
class Meta:
|
|
31
|
+
app_label = 'constec_db'
|
|
32
|
+
db_table = 'core"."user_groups'
|
|
33
|
+
unique_together = [['company', 'name']]
|
|
34
|
+
|
|
35
|
+
def __str__(self):
|
|
36
|
+
return f"{self.name}"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .company import Company
|
|
4
|
+
from .organization import Organization
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Module(UUIDModel):
|
|
8
|
+
"""Available modules in the platform."""
|
|
9
|
+
name = models.CharField(max_length=100)
|
|
10
|
+
slug = models.SlugField(max_length=100, null=True)
|
|
11
|
+
description = models.TextField(blank=True, null=True)
|
|
12
|
+
version = models.CharField(max_length=20)
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
app_label = 'constec_db'
|
|
16
|
+
db_table = 'core"."modules'
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return f"{self.name}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CompanyModule(UUIDModel):
|
|
23
|
+
"""Modules enabled per company."""
|
|
24
|
+
company = models.ForeignKey(
|
|
25
|
+
Company,
|
|
26
|
+
on_delete=models.CASCADE,
|
|
27
|
+
related_name="company_modules",
|
|
28
|
+
)
|
|
29
|
+
module = models.ForeignKey(
|
|
30
|
+
Module,
|
|
31
|
+
on_delete=models.CASCADE,
|
|
32
|
+
related_name="company_modules",
|
|
33
|
+
)
|
|
34
|
+
is_enabled = models.BooleanField(default=True)
|
|
35
|
+
settings = models.JSONField(default=dict, blank=True)
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
app_label = 'constec_db'
|
|
39
|
+
db_table = 'core"."company_modules'
|
|
40
|
+
unique_together = [['company', 'module']]
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return f"{self.company.name} - {self.module.name}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OrganizationModule(UUIDModel):
|
|
47
|
+
"""Modules enabled per organization."""
|
|
48
|
+
organization = models.ForeignKey(
|
|
49
|
+
Organization,
|
|
50
|
+
on_delete=models.CASCADE,
|
|
51
|
+
related_name="organization_modules",
|
|
52
|
+
)
|
|
53
|
+
module = models.ForeignKey(
|
|
54
|
+
Module,
|
|
55
|
+
on_delete=models.CASCADE,
|
|
56
|
+
related_name="organization_modules",
|
|
57
|
+
)
|
|
58
|
+
is_enabled = models.BooleanField(default=True)
|
|
59
|
+
settings = models.JSONField(default=dict, blank=True)
|
|
60
|
+
|
|
61
|
+
class Meta:
|
|
62
|
+
app_label = 'constec_db'
|
|
63
|
+
db_table = 'core"."organization_modules'
|
|
64
|
+
unique_together = [['organization', 'module']]
|
|
65
|
+
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return f"{self.organization.name} - {self.module.name}"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Organization(UUIDModel):
|
|
6
|
+
"""Platform-level tenant (e.g., Constec itself)."""
|
|
7
|
+
name = models.CharField(max_length=255)
|
|
8
|
+
slug = models.SlugField(max_length=100, unique=True)
|
|
9
|
+
description = models.TextField(blank=True, null=True)
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
app_label = 'constec_db'
|
|
13
|
+
db_table = 'core"."organizations'
|
|
14
|
+
|
|
15
|
+
def __str__(self):
|
|
16
|
+
return self.name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OrganizationRole(UUIDModel):
|
|
20
|
+
"""Roles within an organization."""
|
|
21
|
+
organization = models.ForeignKey(
|
|
22
|
+
Organization,
|
|
23
|
+
on_delete=models.CASCADE,
|
|
24
|
+
related_name="organization_roles",
|
|
25
|
+
null=True,
|
|
26
|
+
blank=True
|
|
27
|
+
)
|
|
28
|
+
name = models.CharField(max_length=100)
|
|
29
|
+
description = models.TextField(blank=True, null=True)
|
|
30
|
+
permissions = models.JSONField(default=dict)
|
|
31
|
+
is_system_role = models.BooleanField(default=False)
|
|
32
|
+
|
|
33
|
+
class Meta:
|
|
34
|
+
app_label = 'constec_db'
|
|
35
|
+
db_table = 'core"."organization_roles'
|
|
36
|
+
unique_together = [['organization', 'name']]
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return self.name
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OrganizationUser(UUIDModel):
|
|
43
|
+
"""Administrator users at organization level."""
|
|
44
|
+
organization = models.ForeignKey(
|
|
45
|
+
Organization,
|
|
46
|
+
on_delete=models.CASCADE,
|
|
47
|
+
related_name="users",
|
|
48
|
+
)
|
|
49
|
+
name = models.CharField(max_length=255)
|
|
50
|
+
email = models.EmailField(unique=True)
|
|
51
|
+
password_hash = models.CharField(max_length=255)
|
|
52
|
+
role = models.ForeignKey(
|
|
53
|
+
OrganizationRole,
|
|
54
|
+
on_delete=models.PROTECT
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
class Meta:
|
|
58
|
+
app_label = 'constec_db'
|
|
59
|
+
db_table = 'core"."organization_users'
|
|
60
|
+
|
|
61
|
+
def __str__(self):
|
|
62
|
+
return f"{self.name} ({self.email})"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .company import Company
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Person(UUIDModel):
|
|
7
|
+
"""Real person (customer, supplier, employee)."""
|
|
8
|
+
company = models.ForeignKey(
|
|
9
|
+
Company,
|
|
10
|
+
on_delete=models.CASCADE,
|
|
11
|
+
related_name="persons",
|
|
12
|
+
)
|
|
13
|
+
first_name = models.CharField(max_length=100)
|
|
14
|
+
last_name = models.CharField(max_length=100)
|
|
15
|
+
full_name = models.CharField(max_length=255)
|
|
16
|
+
metadata = models.JSONField(default=dict, blank=True)
|
|
17
|
+
|
|
18
|
+
class Meta:
|
|
19
|
+
app_label = 'constec_db'
|
|
20
|
+
db_table = 'core"."persons'
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return f"{self.full_name}"
|
|
24
|
+
|
|
25
|
+
def save(self, *args, **kwargs):
|
|
26
|
+
"""Auto-generate full_name from first_name and last_name."""
|
|
27
|
+
self.full_name = f"{self.first_name} {self.last_name}"
|
|
28
|
+
super().save(*args, **kwargs)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from .base import UUIDModel
|
|
3
|
+
from .company import Company
|
|
4
|
+
from .person import Person
|
|
5
|
+
from .flow import Flow
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Session(UUIDModel):
|
|
9
|
+
"""Conversation session between a user and the AI agent.
|
|
10
|
+
|
|
11
|
+
Tracks the conversation context including company, user, and permissions.
|
|
12
|
+
All MCP tools receive the session_id to know who is making the request.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
company = models.ForeignKey(
|
|
16
|
+
Company,
|
|
17
|
+
on_delete=models.CASCADE,
|
|
18
|
+
related_name="sessions",
|
|
19
|
+
help_text="The company this session belongs to"
|
|
20
|
+
)
|
|
21
|
+
user = models.ForeignKey(
|
|
22
|
+
Person,
|
|
23
|
+
on_delete=models.CASCADE,
|
|
24
|
+
related_name="sessions",
|
|
25
|
+
help_text="The person chatting (can be employee or client)"
|
|
26
|
+
)
|
|
27
|
+
flow = models.ForeignKey(
|
|
28
|
+
Flow,
|
|
29
|
+
on_delete=models.PROTECT,
|
|
30
|
+
related_name="sessions",
|
|
31
|
+
help_text="AI flow configuration to use"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
metadata = models.JSONField(
|
|
35
|
+
default=dict,
|
|
36
|
+
help_text="Session context (connection, role, etc.)"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
ended_at = models.DateTimeField(null=True, blank=True)
|
|
40
|
+
|
|
41
|
+
class Meta:
|
|
42
|
+
app_label = 'constec_db'
|
|
43
|
+
db_table = 'constancia"."sessions'
|
|
44
|
+
indexes = [
|
|
45
|
+
models.Index(fields=['company', 'is_active']),
|
|
46
|
+
models.Index(fields=['user', 'is_active']),
|
|
47
|
+
models.Index(fields=['-created_at']),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def __str__(self):
|
|
51
|
+
return f"Session {self.id} - {self.user} @ {self.company.name}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Message(UUIDModel):
|
|
55
|
+
"""Message in a conversation session.
|
|
56
|
+
|
|
57
|
+
Stores the conversation history between user and AI assistant.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
ROLE_CHOICES = [
|
|
61
|
+
('user', 'User'),
|
|
62
|
+
('assistant', 'Assistant'),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
session = models.ForeignKey(
|
|
66
|
+
Session,
|
|
67
|
+
on_delete=models.CASCADE,
|
|
68
|
+
related_name="messages",
|
|
69
|
+
)
|
|
70
|
+
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
|
71
|
+
content = models.TextField()
|
|
72
|
+
|
|
73
|
+
metadata = models.JSONField(
|
|
74
|
+
default=dict,
|
|
75
|
+
blank=True,
|
|
76
|
+
help_text="Additional message data: tool calls, errors, context, etc."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
class Meta:
|
|
80
|
+
app_label = 'constec_db'
|
|
81
|
+
db_table = 'constancia"."messages'
|
|
82
|
+
ordering = ['created_at']
|
|
83
|
+
indexes = [
|
|
84
|
+
models.Index(fields=['session', 'created_at']),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
def __str__(self):
|
|
88
|
+
preview = self.content[:50] + "..." if len(self.content) > 50 else self.content
|
|
89
|
+
return f"{self.role}: {preview}"
|