Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions flexus_client_kit/ckit_integrations_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ async def _init_pdoc(rcx, setup):
integr_prompt=fi_pdoc.POLICY_DOCUMENT_PROMPT,
))

elif name == "escalate_to_human":
from flexus_client_kit.integrations import fi_escalate
async def _init_escalate(rcx, setup):
return None
result.append(IntegrationRecord(
integr_name=name,
integr_tools=[fi_escalate.ESCALATE_TO_HUMAN_TOOL],
integr_init=_init_escalate,
integr_setup_handlers=lambda obj, rcx: [
rcx.on_tool_call(fi_escalate.ESCALATE_TO_HUMAN_TOOL.name)(
lambda tc, args, _rcx=rcx: fi_escalate.handle_escalate_to_human(_rcx, tc, args)
)
],
integr_prompt=fi_escalate.ESCALATE_TO_HUMAN_PROMPT,
))

elif name == "print_widget":
from flexus_client_kit.integrations import fi_widget
async def _init_widget(rcx, setup):
Expand Down Expand Up @@ -510,9 +526,9 @@ async def _emessage_handler(emsg, _m=obj):
if rcx.messengers:
@rcx.on_updated_message
async def _messenger_updated_message(msg: ckit_ask_model.FThreadMessageOutput):
# Don't worry, you can override it. The default reaction to assistant messages is to get it past messengers:
# Don't worry, you can override it. The default reaction is to relay assistant messages and flexus-user messages past messengers:
for m in rcx.messengers:
await m.look_assistant_might_have_posted_something(msg)
await m.look_assistant_or_fuser_might_have_posted(msg)
await m.look_user_message_got_confirmed(msg)

return result
8 changes: 6 additions & 2 deletions flexus_client_kit/integrations/fi_discord2.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,8 +778,12 @@ async def post_into_captured_thread_as_user(self, activity: ActivityDiscord) ->
return False
return bool(ft_id)

async def look_assistant_might_have_posted_something(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
if msg.ftm_role != "assistant" or not msg.ftm_content:
async def look_assistant_or_fuser_might_have_posted(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
if msg.ftm_role not in ("assistant", "user"):
return False
if msg.ftm_role == "user" and (msg.ftm_author_label1 or "").startswith("discord:"):
return False
if not msg.ftm_content:
return False
if not msg.ft_app_searchable:
return False
Expand Down
65 changes: 65 additions & 0 deletions flexus_client_kit/integrations/fi_escalate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from typing import Any, Dict

import gql

from flexus_client_kit import ckit_bot_exec, ckit_cloudtool


ESCALATE_TO_HUMAN_PROMPT = """
## Escalation to Human

Before escalating to a human, confirm with the client that they will wait for a human to respond.
NEVER call escalate_to_human without doing so.

After calling escalate_to_human, staff is being notified. Once staff speaks in the thread,
say NOTHING_TO_SAY while they and the client talk. Resume if staff asks.

If staff hasn't arrived and the client keeps writing, a brief calming reply is fine —
but don't promise anything or handle what was escalated.

Don't resolve an escalated task until staff has handled it or tells you to.
""".strip()


ESCALATE_TO_HUMAN_TOOL = ckit_cloudtool.CloudTool(
strict=True,
name="escalate_to_human",
description=(
"Hand off the current thread to a human operator. Call this when the user explicitly asks for a human, "
"on legal/fraud/compliance mentions, obvious frustration, or any situation beyond your authority. "
"Before calling, confirm with the client that they will wait for a human to respond."
),
parameters={
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "Short explanation of why a human is needed (one sentence).",
},
},
"required": ["reason"],
"additionalProperties": False,
},
)


async def handle_escalate_to_human(
rcx: ckit_bot_exec.RobotContext,
toolcall: ckit_cloudtool.FCloudtoolCall,
model_produced_args: Dict[str, Any],
) -> str:
reason = (model_produced_args.get("reason") or "").strip()
if not reason:
return "Error: reason is required"
ktask_id = next((t.ktask_id for t in rcx.latest_tasks.values() if t.ktask_inprogress_ft_id == toolcall.fcall_ft_id), None)
if not ktask_id:
return "No task assigned to this thread — escalate_to_human only works within a task context."
http = await rcx.fclient.use_http_on_behalf(toolcall.connected_persona_id, toolcall.fcall_untrusted_key)
async with http as h:
await h.execute(gql.gql("""
mutation BotRequestHumanEscalation($ktask_id: String!, $status: String!, $reason: String) {
kanban_task_update_escalation_status(ktask_id: $ktask_id, ktask_escalation_status: $status, ktask_escalation_reason: $reason)
}"""),
variable_values={"ktask_id": ktask_id, "status": "ESCALATION_REQUESTED", "reason": reason},
)
return "⏸️ESCALATED_TO_HUMAN\nReason: %s\nA human operator has been requested. Stop acting on this thread." % reason
8 changes: 6 additions & 2 deletions flexus_client_kit/integrations/fi_magic_desk.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,12 @@ async def look_user_message_got_confirmed(self, msg: ckit_ask_model.FThreadMessa
)
return True

async def look_assistant_might_have_posted_something(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
if msg.ftm_role != "assistant" or not msg.ftm_content:
async def look_assistant_or_fuser_might_have_posted(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
if msg.ftm_role not in ("assistant", "user"):
return False
if msg.ftm_role == "user" and (msg.ftm_author_label1 or "").startswith("magic_desk:"):
return False
if not msg.ftm_content:
return False
searchable = msg.ft_app_searchable or ""
if not searchable.startswith("magic_desk/"):
Expand Down
2 changes: 1 addition & 1 deletion flexus_client_kit/integrations/fi_messenger.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def accept_outside_messages_only_to_expert(self, fexp_name: str):
async def handle_emessage(self, emsg: ckit_bot_query.FExternalMessageOutput) -> None:
raise NotImplementedError

async def look_assistant_might_have_posted_something(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
async def look_assistant_or_fuser_might_have_posted(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
raise NotImplementedError

async def close(self) -> None:
Expand Down
16 changes: 10 additions & 6 deletions flexus_client_kit/integrations/fi_slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,10 @@ async def post_into_captured_thread_as_user(self, a: ActivitySlack, channel_id:
logger.info("Captured slack->db ft_id=%s ft_app_searchable=%s sending=%d parts", ft_id, searchable, len(content))
return True

async def look_assistant_might_have_posted_something(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
if msg.ftm_role != "assistant":
async def look_assistant_or_fuser_might_have_posted(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
if msg.ftm_role not in ("assistant", "user"):
return False
if msg.ftm_role == "user" and (msg.ftm_author_label1 or "").startswith("slack:"):
return False
if not msg.ftm_content:
return False
Expand Down Expand Up @@ -613,12 +615,14 @@ async def look_assistant_might_have_posted_something(self, msg: ckit_ask_model.F
if not self.web_client:
return False

text = msg.ftm_content
text = fi_messenger.ftm_content_to_text(msg.ftm_content)
if not text:
return False
if "TASK_COMPLETED" in text and len(text) <= len("TASK_COMPLETED") + 6:
logger.info("look_assistant_might_have_posted_something: ftm_content has TASK_COMPLETED, not posting to slack")
logger.info("look_assistant_or_fuser_might_have_posted: ftm_content has TASK_COMPLETED, not posting to slack")
return False
if "NOTHING_TO_SAY" in text and len(text) <= len("NOTHING_TO_SAY") + 6:
logger.info("look_assistant_might_have_posted_something: ftm_content has NOTHING_TO_SAY, not posting to slack")
logger.info("look_assistant_or_fuser_might_have_posted: ftm_content has NOTHING_TO_SAY, not posting to slack")
return False
text = text.replace("TASK_COMPLETED", "")
text = text.replace("NOTHING_TO_SAY", "")
Expand Down Expand Up @@ -656,7 +660,7 @@ async def look_assistant_might_have_posted_something(self, msg: ckit_ask_model.F
await ckit_ask_model.thread_app_capture_patch(http, msg.ftm_belongs_to_ft_id, ft_app_specific=json.dumps({
"last_posted_assistant_ts": msg.ftm_created_ts
}))
logger.info("/look_assistant_might_have_posted_something() success")
logger.info("/look_assistant_or_fuser_might_have_posted() success")
return True

async def load_workspace_maps(self):
Expand Down
19 changes: 11 additions & 8 deletions flexus_client_kit/integrations/fi_telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,8 +433,12 @@ async def post_into_captured_thread_as_user(self, activity: ActivityTelegram) ->
activity.message_author_name, activity.message_author_id, msg_text[:120] or "(empty)")
return True

async def look_assistant_might_have_posted_something(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
if msg.ftm_role != "assistant" or not msg.ftm_content:
async def look_assistant_or_fuser_might_have_posted(self, msg: ckit_ask_model.FThreadMessageOutput) -> bool:
if msg.ftm_role not in ("assistant", "user"):
return False
if msg.ftm_role == "user" and (msg.ftm_author_label1 or "").startswith("telegram:"):
return False
if not msg.ftm_content:
return False

searchable = msg.ft_app_searchable or ""
Expand All @@ -458,18 +462,17 @@ async def look_assistant_might_have_posted_something(self, msg: ckit_ask_model.F
chat_id = int(searchable[len("telegram/"):])
except ValueError:
return False
if not isinstance(msg.ftm_content, str):
logger.warning("telegram look_assistant_might_have_posted_something: ftm_content is not a string: %r" % msg.ftm_content)
return False
if not self.tg_app:
return False

text = msg.ftm_content
text = fi_messenger.ftm_content_to_text(msg.ftm_content)
if not text:
return False
if "TASK_COMPLETED" in text and len(text) <= len("TASK_COMPLETED") + 6:
logger.info("telegram look_assistant_might_have_posted_something: ftm_content has TASK_COMPLETED, not posting to the captured chat")
logger.info("telegram look_assistant_or_fuser_might_have_posted: ftm_content has TASK_COMPLETED, not posting to the captured chat")
return False
if "NOTHING_TO_SAY" in text and len(text) <= len("NOTHING_TO_SAY") + 6:
logger.info("telegram look_assistant_might_have_posted_something: ftm_content has NOTHING_TO_SAY, not posting to the captured chat")
logger.info("telegram look_assistant_or_fuser_might_have_posted: ftm_content has NOTHING_TO_SAY, not posting to the captured chat")
return False
text = text.replace("TASK_COMPLETED", "") # yes, sometimes the model writes it anyway
text = text.replace("NOTHING_TO_SAY", "")
Expand Down
1 change: 1 addition & 0 deletions flexus_simple_bots/karen/karen_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"skills",
"flexus_policy_document",
"print_widget",
"escalate_to_human",
"erp[meta, data, crud, csv_import]",
"crm[contact_info, manage_deal, verify_email]",
"magic_desk",
Expand Down
1 change: 1 addition & 0 deletions flexus_simple_bots/karen/karen_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"product_catalog", "shopify_cart",
"crm_contact_info", "verify_email",
"email_reply",
"escalate_to_human",
"magic_desk", "slack", "telegram", "discord",
} | ckit_cloudtool.KANBAN_PUBLIC | ckit_cloudtool.CLOUDTOOLS_VECDB | ckit_cloudtool.CLOUDTOOLS_MCP

Expand Down
2 changes: 1 addition & 1 deletion flexus_simple_bots/karen/karen_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
* Don't reveal task IDs, budget, internal processes
* Disclose your AI nature at the start of the conversation. Never pretend to be human.
* Never give legal/medical/financial advice, guarantee outcomes, collect SSN/passwords, or use high-pressure tactics
* Escalate to a human on: legal/fraud mentions, cancellation/refund requests, explicit requests for a human, or if frustration is obvious
* Escalate to a human via escalate_to_human(reason=...) on: legal/fraud mentions, cancellation/refund requests, explicit requests for a human, or if frustration is obvious — then stop acting on the thread

You handle support (existing customers with questions) and sales (prospects exploring the product). Detect which from context.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ async def handle_mongo_store(toolcall: ckit_cloudtool.FCloudtoolCall, args: Dict
@rcx.on_updated_message
async def handle_message(msg):
if tg:
await tg.look_assistant_might_have_posted_something(msg)
await tg.look_assistant_or_fuser_might_have_posted(msg)

@rcx.on_emessage("TELEGRAM")
async def handle_emessage(emsg):
Expand Down