Fix Signal messages and replies
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user