mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
Mechanically re-derived: bin/debrand runs over a fresh import of posthog/posthog@fa1a9a60d4 (2026-08-06). The pipeline (.hanzo/workflows), the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md and the squashed initial migration are restored from the shipped tree, same as the previous import. The prune's import-closure guard kept 21 products the new core now imports; dropping them is a refactor now, recorded rather than forced.
213 lines
8.8 KiB
Python
213 lines
8.8 KiB
Python
from django.db import models
|
|
from django.db.models import Q
|
|
|
|
from insights.models.utils import UUIDModel
|
|
|
|
|
|
class SlackThreadTaskMapping(UUIDModel):
|
|
"""Maps Slack threads to task runs so follow-up messages can be forwarded to the running agent."""
|
|
|
|
team = models.ForeignKey("insights.Team", on_delete=models.CASCADE, related_name="slack_thread_task_mappings")
|
|
integration = models.ForeignKey(
|
|
"insights.Integration",
|
|
on_delete=models.CASCADE,
|
|
related_name="slack_thread_task_mappings",
|
|
)
|
|
slack_workspace_id = models.CharField(max_length=64)
|
|
channel = models.CharField(max_length=64)
|
|
thread_ts = models.CharField(max_length=64)
|
|
task = models.ForeignKey(
|
|
"tasks.Task",
|
|
on_delete=models.CASCADE,
|
|
related_name="slack_thread_mappings",
|
|
)
|
|
task_run = models.ForeignKey(
|
|
"tasks.TaskRun",
|
|
on_delete=models.CASCADE,
|
|
related_name="slack_thread_mappings",
|
|
)
|
|
mentioning_slack_user_id = models.CharField(max_length=64)
|
|
# Reply-tag fallback for runs started before per-turn actor capture
|
|
# (tasks slack_relay); drop the column and its stamp in task_creation
|
|
# once those runs drain.
|
|
latest_actor_slack_user_id = models.CharField(max_length=64, null=True, blank=True)
|
|
# Slack `ts` of the most recent message we've already shown to the agent (either
|
|
# in the original `<slack_thread_context>` block at task creation, or in a follow-up
|
|
# `<slack_thread_context_update>` diff). On each follow-up, anything in the thread
|
|
# with a strictly larger `ts` (and smaller than the just-arrived message's `ts`) is
|
|
# rendered as a diff so the agent catches up on messages it never saw.
|
|
last_forwarded_ts = models.CharField(max_length=64, null=True, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["integration", "channel", "thread_ts"],
|
|
name="uniq_slack_thread_task_mapping",
|
|
)
|
|
]
|
|
indexes = [
|
|
# Serves the workspace-wide activity aggregates on the App Home tab, which scan a
|
|
# whole workspace over a date window rather than a single thread. `team_id` rides
|
|
# along as an INCLUDE column so the accessible-projects filter is evaluated off the
|
|
# index instead of a heap fetch per candidate row.
|
|
models.Index(
|
|
fields=["slack_workspace_id", "created_at"],
|
|
include=["team_id"],
|
|
name="slack_thr_map_ws_created_idx",
|
|
),
|
|
]
|
|
|
|
|
|
class SlackUserProfileCache(UUIDModel):
|
|
integration = models.ForeignKey(
|
|
"insights.Integration",
|
|
on_delete=models.CASCADE,
|
|
related_name="slack_user_profile_cache",
|
|
)
|
|
slack_user_id = models.CharField(max_length=64)
|
|
email = models.EmailField(blank=True, null=True)
|
|
display_name = models.CharField(max_length=255, blank=True, default="")
|
|
real_name = models.CharField(max_length=255, blank=True, default="")
|
|
is_admin = models.BooleanField(default=False, db_default=False)
|
|
is_owner = models.BooleanField(default=False, db_default=False)
|
|
is_bot = models.BooleanField(default=False, db_default=False)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
# Null is treated as stale (rows predating this field).
|
|
refreshed_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["integration", "slack_user_id"],
|
|
name="uniq_slack_user_profile_cache_integration_user",
|
|
)
|
|
]
|
|
|
|
|
|
class SlackSettings(UUIDModel):
|
|
"""Per-(Slack workspace, Slack user) settings for inbound Slack events.
|
|
Currently stores the routing default — which Insights integration a mention
|
|
from this Slack user should route to.
|
|
|
|
Two row shapes share this table:
|
|
- ``slack_user_id`` set → that Slack user's personal settings for this workspace.
|
|
Written by the Slack `@Insights project <id>` directive or the user-level
|
|
settings UI.
|
|
- ``slack_user_id IS NULL`` → workspace-wide fallback, applied when an
|
|
inbound event's Slack user has no personal row yet. Written via the
|
|
Insights project-level settings UI by a team admin, or via the Slack
|
|
`@Insights project workspace <id>` directive by a Slack workspace
|
|
admin/owner.
|
|
|
|
A user-specific row, if present, always wins over the workspace-wide row at
|
|
resolution time.
|
|
"""
|
|
|
|
# Nullable so a personal row can carry AI preferences while inheriting the
|
|
# workspace routing default.
|
|
default_integration = models.ForeignKey(
|
|
"insights.Integration",
|
|
on_delete=models.CASCADE,
|
|
related_name="slack_settings_as_default",
|
|
null=True,
|
|
blank=True,
|
|
)
|
|
slack_workspace_id = models.CharField(max_length=64)
|
|
slack_user_id = models.CharField(max_length=64, null=True, blank=True)
|
|
# Unused: holds historical per-integration agent permission modes from the retired
|
|
# approval-card flow. Retained so the column needs no migration.
|
|
permission_modes = models.JSONField(
|
|
blank=True,
|
|
null=True,
|
|
help_text="Per-integration permission mode for Slack-started agent runs, keyed by integration id.",
|
|
)
|
|
# Keys mirror the task-run request serializer.
|
|
ai_preferences = models.JSONField(blank=True, null=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["slack_workspace_id", "slack_user_id"],
|
|
name="uniq_slack_settings_per_user",
|
|
condition=Q(slack_user_id__isnull=False),
|
|
),
|
|
# Partial: NULL slack_user_id is the workspace-wide row, which
|
|
# Postgres's default NULL-distinct rule wouldn't otherwise dedupe.
|
|
models.UniqueConstraint(
|
|
fields=["slack_workspace_id"],
|
|
name="uniq_slack_settings_per_workspace",
|
|
condition=Q(slack_user_id__isnull=True),
|
|
),
|
|
]
|
|
|
|
def __str__(self) -> str:
|
|
who = self.slack_user_id or "(workspace default)"
|
|
target = self.default_integration_id if self.default_integration_id else "(inherit)"
|
|
return f"{self.slack_workspace_id} / {who} → integration {target}"
|
|
|
|
@property
|
|
def runtime_adapter(self) -> str | None:
|
|
return (self.ai_preferences or {}).get("runtime_adapter")
|
|
|
|
@property
|
|
def model(self) -> str | None:
|
|
return (self.ai_preferences or {}).get("model")
|
|
|
|
@property
|
|
def reasoning_effort(self) -> str | None:
|
|
return (self.ai_preferences or {}).get("reasoning_effort")
|
|
|
|
|
|
class SlackChannel(UUIDModel):
|
|
"""Per-(Slack workspace, Slack channel) state.
|
|
|
|
Today the only meaning of a row with ``approved_at`` set is that a user
|
|
in the channel has acknowledged that Insights data answered there may be
|
|
visible to external (cross-workspace) members. Absence of a row — or a
|
|
row with ``approved_at`` null — for an externally-shared channel means
|
|
the bot must refuse to answer and post the approval prompt instead.
|
|
Non-externally-shared channels skip the lookup entirely.
|
|
|
|
The approval is workspace-scoped, not integration-scoped: a single
|
|
workspace can be connected to multiple Insights projects, and once any
|
|
member of any of those orgs has consented to the bot answering in this
|
|
channel the consent applies to all of them. Per-mention data access
|
|
remains gated by the existing email → ``OrganizationMembership`` check,
|
|
so workspace-scope here only governs the channel-level question
|
|
("can the bot speak here at all?"), not which project's data is read.
|
|
|
|
Reserved for future per-channel configuration (denials, routing
|
|
overrides, etc.) without needing another model.
|
|
"""
|
|
|
|
slack_workspace_id = models.CharField(max_length=64)
|
|
slack_channel_id = models.CharField(max_length=64)
|
|
approved_at = models.DateTimeField(null=True, blank=True)
|
|
approved_by = models.ForeignKey(
|
|
"insights.User",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="approved_slack_channels",
|
|
help_text="The Insights user who clicked Approve. Carries the email and audit trail.",
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["slack_workspace_id", "slack_channel_id"],
|
|
name="uniq_slack_channel",
|
|
)
|
|
]
|
|
|
|
@property
|
|
def is_approved(self) -> bool:
|
|
return self.approved_at is not None
|