Fix business plans
This commit is contained in:
@@ -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()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user