Fix Signal messages and replies

This commit is contained in:
2026-03-03 15:51:58 +00:00
parent 56c620473f
commit d6bd56dace
31 changed files with 3317 additions and 668 deletions

View File

@@ -8,6 +8,7 @@ from django.conf import settings
from core.commands.base import CommandContext, CommandHandler, CommandResult
from core.commands.delivery import post_status_in_source, post_to_channel_binding
from core.commands.policies import BP_VARIANT_META, load_variant_policy
from core.messaging import ai as ai_runner
from core.messaging.text_export import plain_text_blob
from core.messaging.utils import messages_to_string
@@ -18,6 +19,7 @@ from core.models import (
CommandAction,
CommandChannelBinding,
CommandRun,
CommandVariantPolicy,
Message,
)
@@ -91,6 +93,45 @@ def _clamp_transcript(transcript: str, max_chars: int) -> str:
class BPCommandHandler(CommandHandler):
slug = "bp"
def _variant_key_for_text(self, text: str) -> str:
parsed = parse_bp_subcommand(text)
if parsed.command == "set":
return "bp_set"
if parsed.command == "set_range":
return "bp_set_range"
return "bp"
def _variant_display_name(self, variant_key: str) -> str:
meta = BP_VARIANT_META.get(str(variant_key or "").strip(), {})
return str(meta.get("name") or variant_key or "bp")
async def _effective_policy(
self,
*,
profile,
variant_key: str,
action_types: set[str],
) -> dict:
policy = await sync_to_async(load_variant_policy)(profile, variant_key)
if isinstance(policy, CommandVariantPolicy):
return {
"enabled": bool(policy.enabled),
"generation_mode": str(policy.generation_mode or "verbatim"),
"send_plan_to_egress": bool(policy.send_plan_to_egress)
and ("post_result" in action_types),
"send_status_to_source": bool(policy.send_status_to_source),
"send_status_to_egress": bool(policy.send_status_to_egress),
"store_document": bool(getattr(policy, "store_document", True)),
}
return {
"enabled": True,
"generation_mode": "ai" if variant_key == "bp" else "verbatim",
"send_plan_to_egress": "post_result" in action_types,
"send_status_to_source": str(profile.visibility_mode or "") == "status_in_source",
"send_status_to_egress": False,
"store_document": True,
}
async def _fanout(self, run: CommandRun, text: str) -> dict:
profile = run.profile
trigger = await sync_to_async(
@@ -124,6 +165,39 @@ class BPCommandHandler(CommandHandler):
failed_bindings += 1
return {"sent_bindings": sent_bindings, "failed_bindings": failed_bindings}
async def _fanout_status(self, run: CommandRun, text: str) -> dict:
profile = run.profile
trigger = await sync_to_async(
lambda: Message.objects.select_related("session", "user")
.filter(id=run.trigger_message_id)
.first()
)()
if trigger is None:
return {"sent_bindings": 0, "failed_bindings": 0}
bindings = await sync_to_async(list)(
CommandChannelBinding.objects.filter(
profile=profile,
enabled=True,
direction="egress",
)
)
sent_bindings = 0
failed_bindings = 0
for binding in bindings:
ok = await post_to_channel_binding(
trigger_message=trigger,
binding_service=binding.service,
binding_channel_identifier=binding.channel_identifier,
text=text,
origin_tag=f"bp-status-egress:{run.id}",
command_slug=self.slug,
)
if ok:
sent_bindings += 1
else:
failed_bindings += 1
return {"sent_bindings": sent_bindings, "failed_bindings": failed_bindings}
async def _load_window(self, trigger: Message, anchor: Message) -> list[Message]:
return await sync_to_async(list)(
Message.objects.filter(
@@ -188,7 +262,8 @@ class BPCommandHandler(CommandHandler):
trigger: Message,
run: CommandRun,
profile,
action_types: set[str],
policy: dict,
variant_key: str,
parsed: BPParsedCommand,
) -> CommandResult:
mode = str(parsed.command or "")
@@ -202,23 +277,63 @@ class BPCommandHandler(CommandHandler):
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
rows = await self._load_window(trigger, anchor)
content = plain_text_blob(rows)
if not content.strip():
deterministic_content = plain_text_blob(rows)
if not deterministic_content.strip():
run.status = "failed"
run.error = "bp_set_range_empty_content"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
if str(policy.get("generation_mode") or "verbatim") == "ai":
ai_obj = await sync_to_async(lambda: AI.objects.filter(user=trigger.user).first())()
if ai_obj is None:
run.status = "failed"
run.error = "ai_not_configured"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
prompt = [
{
"role": "system",
"content": (
"Transform source chat text into a structured business plan in markdown. "
"Do not reference any user template."
),
},
{"role": "user", "content": deterministic_content},
]
try:
content = str(
await ai_runner.run_prompt(
prompt,
ai_obj,
operation="command_bp_set_range_extract",
)
or ""
).strip()
except Exception as exc:
run.status = "failed"
run.error = f"bp_ai_failed:{exc}"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
if not content:
run.status = "failed"
run.error = "empty_ai_response"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
else:
content = deterministic_content
annotation = self._annotation("set_range", len(rows))
doc = await self._persist_document(
run=run,
trigger=trigger,
profile=profile,
anchor=anchor,
content=content,
mode="set_range",
source_message_ids=[str(row.id) for row in rows],
annotation=annotation,
)
doc = None
if bool(policy.get("store_document", True)):
doc = await self._persist_document(
run=run,
trigger=trigger,
profile=profile,
anchor=anchor,
content=content,
mode="set_range",
source_message_ids=[str(row.id) for row in rows],
annotation=annotation,
)
elif mode == "set":
source_ids: list[str] = []
if anchor is not None and not remainder:
@@ -244,17 +359,57 @@ class BPCommandHandler(CommandHandler):
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
if str(policy.get("generation_mode") or "verbatim") == "ai":
ai_obj = await sync_to_async(lambda: AI.objects.filter(user=trigger.user).first())()
if ai_obj is None:
run.status = "failed"
run.error = "ai_not_configured"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
prompt = [
{
"role": "system",
"content": (
"Transform source chat text into a structured business plan in markdown. "
"Do not reference any user template."
),
},
{"role": "user", "content": content},
]
try:
ai_content = str(
await ai_runner.run_prompt(
prompt,
ai_obj,
operation="command_bp_set_extract",
)
or ""
).strip()
except Exception as exc:
run.status = "failed"
run.error = f"bp_ai_failed:{exc}"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
if not ai_content:
run.status = "failed"
run.error = "empty_ai_response"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
content = ai_content
annotation = self._annotation("set", 1 if not has_addendum else 2, has_addendum)
doc = await self._persist_document(
run=run,
trigger=trigger,
profile=profile,
anchor=anchor,
content=content,
mode="set",
source_message_ids=source_ids,
annotation=annotation,
)
doc = None
if bool(policy.get("store_document", True)):
doc = await self._persist_document(
run=run,
trigger=trigger,
profile=profile,
anchor=anchor,
content=content,
mode="set",
source_message_ids=source_ids,
annotation=annotation,
)
else:
run.status = "failed"
run.error = "bp_unknown_subcommand"
@@ -262,31 +417,38 @@ class BPCommandHandler(CommandHandler):
return CommandResult(ok=False, status="failed", error=run.error)
fanout_stats = {"sent_bindings": 0, "failed_bindings": 0}
if "post_result" in action_types:
fanout_body = f"{doc.content_markdown}\n\n{doc.structured_payload.get('annotation', '')}".strip()
if bool(policy.get("send_plan_to_egress")):
fanout_body = f"{content}\n\n{annotation}".strip()
fanout_stats = await self._fanout(run, fanout_body)
if "status_in_source" == profile.visibility_mode:
status_text = (
f"[bp] {doc.structured_payload.get('annotation', '').strip()} "
f"Saved as {doc.title}."
).strip()
sent_count = int(fanout_stats.get("sent_bindings") or 0)
failed_count = int(fanout_stats.get("failed_bindings") or 0)
if sent_count or failed_count:
status_text += f" · fanout sent:{sent_count}"
if failed_count:
status_text += f" failed:{failed_count}"
sent_count = int(fanout_stats.get("sent_bindings") or 0)
failed_count = int(fanout_stats.get("failed_bindings") or 0)
status_text = (
f"[bp:{self._variant_display_name(variant_key)}:{policy.get('generation_mode')}] "
f"{annotation.strip()} "
f"{'Saved as ' + doc.title + ' · ' if doc else 'Not saved (store_document disabled) · '}"
f"fanout sent:{sent_count}"
).strip()
if failed_count:
status_text += f" failed:{failed_count}"
if bool(policy.get("send_status_to_source")):
await post_status_in_source(
trigger_message=trigger,
text=status_text,
origin_tag=f"bp-status:{trigger.id}",
)
if bool(policy.get("send_status_to_egress")):
await self._fanout_status(run, status_text)
run.status = "ok"
run.error = ""
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=True, status="ok", payload={"document_id": str(doc.id)})
return CommandResult(
ok=True,
status="ok",
payload={"document_id": str(doc.id) if doc else ""},
)
async def _execute_legacy_ai(
self,
@@ -294,8 +456,8 @@ class BPCommandHandler(CommandHandler):
trigger: Message,
run: CommandRun,
profile,
action_types: set[str],
ctx: CommandContext,
policy: dict,
variant_key: str,
) -> CommandResult:
if trigger.reply_to_id is None:
run.status = "failed"
@@ -322,69 +484,90 @@ class BPCommandHandler(CommandHandler):
template_text = profile.template_text or default_template
max_template_chars = int(getattr(settings, "BP_MAX_TEMPLATE_CHARS", 5000) or 5000)
template_text = str(template_text or "")[:max_template_chars]
ai_obj = await sync_to_async(lambda: AI.objects.filter(user=trigger.user).first())()
if ai_obj is None:
run.status = "failed"
run.error = "ai_not_configured"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
generation_mode = str(policy.get("generation_mode") or "ai")
if generation_mode == "verbatim":
summary = plain_text_blob(rows)
if not summary.strip():
run.status = "failed"
run.error = "bp_verbatim_empty_content"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
else:
ai_obj = await sync_to_async(lambda: AI.objects.filter(user=trigger.user).first())()
if ai_obj is None:
run.status = "failed"
run.error = "ai_not_configured"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
prompt = [
{"role": "system", "content": _bp_system_prompt()},
{
"role": "user",
"content": (
"Template:\n"
f"{template_text}\n\n"
"Messages:\n"
f"{transcript}"
),
},
]
try:
summary = str(await ai_runner.run_prompt(prompt, ai_obj, operation="command_bp_extract") or "").strip()
if not summary:
raise RuntimeError("empty_ai_response")
except Exception as exc:
run.status = "failed"
run.error = f"bp_ai_failed:{exc}"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
prompt = [
{"role": "system", "content": _bp_system_prompt()},
{
"role": "user",
"content": (
"Template:\n"
f"{template_text}\n\n"
"Messages:\n"
f"{transcript}"
),
},
]
try:
summary = str(await ai_runner.run_prompt(prompt, ai_obj, operation="command_bp_extract") or "").strip()
if not summary:
raise RuntimeError("empty_ai_response")
except Exception as exc:
run.status = "failed"
run.error = f"bp_ai_failed:{exc}"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="failed", error=run.error)
annotation = self._annotation("legacy", len(rows))
document = await self._persist_document(
run=run,
trigger=trigger,
profile=profile,
anchor=anchor,
content=summary,
mode="legacy_ai",
source_message_ids=[str(row.id) for row in rows],
annotation=annotation,
)
document = None
if bool(policy.get("store_document", True)):
document = await self._persist_document(
run=run,
trigger=trigger,
profile=profile,
anchor=anchor,
content=summary,
mode="legacy_ai",
source_message_ids=[str(row.id) for row in rows],
annotation=annotation,
)
fanout_stats = {"sent_bindings": 0, "failed_bindings": 0}
if "post_result" in action_types:
if bool(policy.get("send_plan_to_egress")):
fanout_stats = await self._fanout(run, summary)
if "status_in_source" == profile.visibility_mode:
status_text = f"[bp] Generated business plan: {document.title}"
sent_count = int(fanout_stats.get("sent_bindings") or 0)
failed_count = int(fanout_stats.get("failed_bindings") or 0)
if sent_count or failed_count:
status_text += f" · fanout sent:{sent_count}"
if failed_count:
status_text += f" failed:{failed_count}"
sent_count = int(fanout_stats.get("sent_bindings") or 0)
failed_count = int(fanout_stats.get("failed_bindings") or 0)
status_text = (
f"[bp:{self._variant_display_name(variant_key)}:{generation_mode}] "
f"Generated business plan: "
f"{document.title if document else 'not saved (store_document disabled)'} "
f"· fanout sent:{sent_count}"
)
if failed_count:
status_text += f" failed:{failed_count}"
if bool(policy.get("send_status_to_source")):
await post_status_in_source(
trigger_message=trigger,
text=status_text,
origin_tag=f"bp-status:{trigger.id}",
)
if bool(policy.get("send_status_to_egress")):
await self._fanout_status(run, status_text)
run.status = "ok"
run.error = ""
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=True, status="ok", payload={"document_id": str(document.id)})
return CommandResult(
ok=True,
status="ok",
payload={"document_id": str(document.id) if document else ""},
)
async def execute(self, ctx: CommandContext) -> CommandResult:
trigger = await sync_to_async(
@@ -418,13 +601,26 @@ class BPCommandHandler(CommandHandler):
run.error = ""
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
variant_key = self._variant_key_for_text(ctx.message_text)
policy = await self._effective_policy(
profile=profile,
variant_key=variant_key,
action_types=action_types,
)
if not bool(policy.get("enabled")):
run.status = "skipped"
run.error = f"variant_disabled:{variant_key}"
await sync_to_async(run.save)(update_fields=["status", "error", "updated_at"])
return CommandResult(ok=False, status="skipped", error=run.error)
parsed = parse_bp_subcommand(ctx.message_text)
if parsed.command and bool(getattr(settings, "BP_SUBCOMMANDS_V1", True)):
return await self._execute_set_or_range(
trigger=trigger,
run=run,
profile=profile,
action_types=action_types,
policy=policy,
variant_key=variant_key,
parsed=parsed,
)
@@ -432,6 +628,6 @@ class BPCommandHandler(CommandHandler):
trigger=trigger,
run=run,
profile=profile,
action_types=action_types,
ctx=ctx,
policy=policy,
variant_key=variant_key,
)

106
core/commands/policies.py Normal file
View File

@@ -0,0 +1,106 @@
from __future__ import annotations
from typing import Iterable
from core.models import CommandAction, CommandProfile, CommandVariantPolicy
BP_VARIANT_KEYS = ("bp", "bp_set", "bp_set_range")
BP_VARIANT_META = {
"bp": {
"name": "bp",
"trigger_token": "#bp#",
"template_supported": True,
"position": 0,
},
"bp_set": {
"name": "bp set",
"trigger_token": "#bp set#",
"template_supported": False,
"position": 1,
},
"bp_set_range": {
"name": "bp set range",
"trigger_token": "#bp set range#",
"template_supported": False,
"position": 2,
},
}
def _legacy_defaults(profile: CommandProfile, post_result_enabled: bool) -> dict:
return {
"enabled": True,
"generation_mode": "ai",
"send_plan_to_egress": bool(post_result_enabled),
"send_status_to_source": str(profile.visibility_mode or "") == "status_in_source",
"send_status_to_egress": False,
"store_document": True,
}
def _bp_defaults(
profile: CommandProfile,
variant_key: str,
post_result_enabled: bool,
) -> dict:
defaults = _legacy_defaults(profile, post_result_enabled)
if variant_key in {"bp_set", "bp_set_range"}:
defaults["generation_mode"] = "verbatim"
else:
defaults["generation_mode"] = "ai"
return defaults
def ensure_variant_policies_for_profile(
profile: CommandProfile,
*,
action_rows: Iterable[CommandAction] | None = None,
) -> dict[str, CommandVariantPolicy]:
actions = list(action_rows) if action_rows is not None else list(profile.actions.all())
post_result_enabled = any(
row.action_type == "post_result" and bool(row.enabled) for row in actions
)
result: dict[str, CommandVariantPolicy] = {}
if str(profile.slug or "").strip() == "bp":
for key in BP_VARIANT_KEYS:
meta = BP_VARIANT_META.get(key, {})
defaults = _bp_defaults(profile, key, post_result_enabled)
policy, _ = CommandVariantPolicy.objects.get_or_create(
profile=profile,
variant_key=key,
defaults={
**defaults,
"position": int(meta.get("position") or 0),
},
)
result[key] = policy
else:
defaults = _legacy_defaults(profile, post_result_enabled)
policy, _ = CommandVariantPolicy.objects.get_or_create(
profile=profile,
variant_key="default",
defaults={
**defaults,
"generation_mode": "verbatim",
"position": 0,
},
)
result["default"] = policy
return result
def load_variant_policy(profile: CommandProfile, variant_key: str) -> CommandVariantPolicy | None:
key = str(variant_key or "").strip()
if not key:
return None
policy = (
profile.variant_policies.filter(variant_key=key)
.order_by("position", "id")
.first()
)
if policy is not None:
return policy
ensured = ensure_variant_policies_for_profile(profile)
return ensured.get(key)