Fix business plans

This commit is contained in:
2026-03-02 00:23:27 +00:00
parent b3e183eb0a
commit a9f5f3f75d
5 changed files with 117 additions and 9 deletions

View File

@@ -15,6 +15,22 @@ log = logs.get_logger("command_engine")
_REGISTERED = False _REGISTERED = False
def _channel_variants(service: str, channel_identifier: str) -> list[str]:
value = str(channel_identifier or "").strip()
if not value:
return []
variants = [value]
svc = str(service or "").strip().lower()
if svc == "whatsapp":
bare = value.split("@", 1)[0].strip()
if bare and bare not in variants:
variants.append(bare)
group = f"{bare}@g.us" if bare else ""
if group and group not in variants:
variants.append(group)
return variants
def ensure_handlers_registered(): def ensure_handlers_registered():
global _REGISTERED global _REGISTERED
if _REGISTERED: if _REGISTERED:
@@ -25,6 +41,9 @@ def ensure_handlers_registered():
async def _eligible_profiles(ctx: CommandContext) -> list[CommandProfile]: async def _eligible_profiles(ctx: CommandContext) -> list[CommandProfile]:
def _load(): def _load():
direct_variants = _channel_variants(ctx.service, ctx.channel_identifier)
if not direct_variants:
return []
direct = list( direct = list(
CommandProfile.objects.filter( CommandProfile.objects.filter(
user_id=ctx.user_id, user_id=ctx.user_id,
@@ -32,7 +51,7 @@ async def _eligible_profiles(ctx: CommandContext) -> list[CommandProfile]:
channel_bindings__enabled=True, channel_bindings__enabled=True,
channel_bindings__direction="ingress", channel_bindings__direction="ingress",
channel_bindings__service=ctx.service, channel_bindings__service=ctx.service,
channel_bindings__channel_identifier=ctx.channel_identifier, channel_bindings__channel_identifier__in=direct_variants,
).distinct() ).distinct()
) )
if direct: if direct:
@@ -49,7 +68,8 @@ async def _eligible_profiles(ctx: CommandContext) -> list[CommandProfile]:
identifier = getattr(getattr(trigger, "session", None), "identifier", None) identifier = getattr(getattr(trigger, "session", None), "identifier", None)
fallback_service = str(getattr(identifier, "service", "") or "").strip().lower() fallback_service = str(getattr(identifier, "service", "") or "").strip().lower()
fallback_identifier = str(getattr(identifier, "identifier", "") or "").strip() fallback_identifier = str(getattr(identifier, "identifier", "") or "").strip()
if not fallback_service or not fallback_identifier: fallback_variants = _channel_variants(fallback_service, fallback_identifier)
if not fallback_service or not fallback_variants:
return [] return []
return list( return list(
CommandProfile.objects.filter( CommandProfile.objects.filter(
@@ -58,7 +78,7 @@ async def _eligible_profiles(ctx: CommandContext) -> list[CommandProfile]:
channel_bindings__enabled=True, channel_bindings__enabled=True,
channel_bindings__direction="ingress", channel_bindings__direction="ingress",
channel_bindings__service=fallback_service, channel_bindings__service=fallback_service,
channel_bindings__channel_identifier=fallback_identifier, channel_bindings__channel_identifier__in=fallback_variants,
).distinct() ).distinct()
) )

View File

@@ -330,8 +330,17 @@ class BPCommandHandler(CommandHandler):
) )
fanout_stats = {"sent_bindings": 0, "failed_bindings": 0} fanout_stats = {"sent_bindings": 0, "failed_bindings": 0}
fanout_text = summary
if ai_warning:
warning_text = str(ai_warning or "").strip()
if len(warning_text) > 300:
warning_text = warning_text[:297].rstrip() + "..."
fanout_text = (
"[bp] AI generation failed. Draft document was saved in fallback mode."
+ (f"\nReason: {warning_text}" if warning_text else "")
)
if "post_result" in action_types: if "post_result" in action_types:
fanout_stats = await self._fanout(run, summary) fanout_stats = await self._fanout(run, fanout_text)
if "status_in_source" == profile.visibility_mode: if "status_in_source" == profile.visibility_mode:
status_text = f"[bp] Generated business plan: {document.title}" status_text = f"[bp] Generated business plan: {document.title}"

View File

@@ -3095,7 +3095,16 @@
} }
setCardLoading(card, true); setCardLoading(card, true);
try { try {
const response = await fetch(thread.dataset.draftsUrl + "?" + queryParams().toString(), { const params = queryParams();
try {
const current = new URL(window.location.href);
if (String(current.searchParams.get("nocache") || "").trim() === "1") {
params.set("nocache", "1");
}
} catch (err) {
// Ignore URL parse errors.
}
const response = await fetch(thread.dataset.draftsUrl + "?" + params.toString(), {
method: "GET", method: "GET",
credentials: "same-origin", credentials: "same-origin",
headers: { Accept: "application/json" } headers: { Accept: "application/json" }
@@ -3109,6 +3118,12 @@
const drafts = Array.isArray(payload.drafts) ? payload.drafts : []; const drafts = Array.isArray(payload.drafts) ? payload.drafts : [];
const container = card.querySelector(".compose-ai-content"); const container = card.querySelector(".compose-ai-content");
container.innerHTML = ""; container.innerHTML = "";
const sourceTag = document.createElement("p");
sourceTag.className = "is-size-7 has-text-grey";
const source = String(payload.source || "unknown");
const cachedLabel = payload.cached ? "cache" : "live";
sourceTag.textContent = "Source: " + source + " (" + cachedLabel + ")";
container.appendChild(sourceTag);
const engageButton = document.createElement("button"); const engageButton = document.createElement("button");
engageButton.type = "button"; engageButton.type = "button";
engageButton.className = "button is-link is-light compose-draft-option"; engageButton.className = "button is-link is-light compose-draft-option";

View File

@@ -239,3 +239,36 @@ class Phase1CommandEngineTests(TestCase):
) )
) )
self.assertEqual([], results) self.assertEqual([], results)
def test_eligible_profile_matches_whatsapp_group_identifier_variants(self):
self.profile.channel_bindings.all().delete()
CommandChannelBinding.objects.create(
profile=self.profile,
direction="ingress",
service="whatsapp",
channel_identifier="120363402761690215@g.us",
enabled=True,
)
msg = Message.objects.create(
user=self.user,
session=self.session,
sender_uuid="",
text="#bp#",
ts=5000,
source_service="whatsapp",
source_chat_id="120363402761690215@g.us",
message_meta={},
)
results = async_to_sync(process_inbound_message)(
CommandContext(
service="whatsapp",
channel_identifier="120363402761690215",
message_id=str(msg.id),
user_id=self.user.id,
message_text="#bp#",
payload={},
)
)
self.assertEqual(1, len(results))
self.assertEqual("skipped", results[0].status)
self.assertEqual("reply_required", results[0].error)

View File

@@ -3346,9 +3346,16 @@ class ComposeDrafts(LoginRequiredMixin, View):
{ {
"ok": True, "ok": True,
"cached": False, "cached": False,
"source": "fallback",
"drafts": _fallback_drafts(), "drafts": _fallback_drafts(),
} }
) )
nocache = str(request.GET.get("nocache") or "").strip().lower() in {
"1",
"true",
"yes",
"on",
}
last_ts = int(messages[-1].ts or 0) last_ts = int(messages[-1].ts or 0)
cache_key = _compose_ai_cache_key( cache_key = _compose_ai_cache_key(
@@ -3361,8 +3368,24 @@ class ComposeDrafts(LoginRequiredMixin, View):
limit, limit,
) )
cached = cache.get(cache_key) cached = cache.get(cache_key)
if cached: if cached and not nocache:
return JsonResponse({"ok": True, "cached": True, "drafts": cached}) if isinstance(cached, dict):
return JsonResponse(
{
"ok": True,
"cached": True,
"source": str(cached.get("source") or "unknown"),
"drafts": list(cached.get("drafts") or []),
}
)
return JsonResponse(
{
"ok": True,
"cached": True,
"source": "unknown",
"drafts": list(cached or []),
}
)
ai_obj = AI.objects.filter(user=request.user).first() ai_obj = AI.objects.filter(user=request.user).first()
transcript = messages_to_string( transcript = messages_to_string(
@@ -3373,6 +3396,7 @@ class ComposeDrafts(LoginRequiredMixin, View):
}, },
) )
drafts = _fallback_drafts() drafts = _fallback_drafts()
source = "fallback"
if ai_obj is not None: if ai_obj is not None:
try: try:
result = async_to_sync(ai_runner.run_prompt)( result = async_to_sync(ai_runner.run_prompt)(
@@ -3386,11 +3410,18 @@ class ComposeDrafts(LoginRequiredMixin, View):
parsed = _parse_draft_options(result) parsed = _parse_draft_options(result)
if parsed: if parsed:
drafts = parsed drafts = parsed
source = "ai"
except Exception: except Exception:
pass pass
cache.set(cache_key, drafts, timeout=COMPOSE_AI_CACHE_TTL) cache.set(
return JsonResponse({"ok": True, "cached": False, "drafts": drafts}) cache_key,
{"source": source, "drafts": drafts},
timeout=COMPOSE_AI_CACHE_TTL,
)
return JsonResponse(
{"ok": True, "cached": False, "source": source, "drafts": drafts}
)
class ComposeSummary(LoginRequiredMixin, View): class ComposeSummary(LoginRequiredMixin, View):