diff --git a/components/experiements/activity_progress.py b/components/experiements/activity_progress.py new file mode 100644 index 0000000..fd5af01 --- /dev/null +++ b/components/experiements/activity_progress.py @@ -0,0 +1,518 @@ +from typing import Any, Dict, List +import json + +import pydivkit as dk +from pydivkit.core import Expr + + +BooleanVariable = dk.BooleanVariable +IntegerVariable = dk.IntegerVariable +StringVariable = dk.StringVariable +DivAction = dk.DivAction +DivBorder = dk.DivBorder +DivCircleShape = dk.DivCircleShape +DivContainer = dk.DivContainer +DivContainerOrientation = dk.DivContainerOrientation +DivEdgeInsets = dk.DivEdgeInsets +DivFixedSize = dk.DivFixedSize +DivIndicator = dk.DivIndicator +DivMatchParentSize = dk.DivMatchParentSize +DivSolidBackground = dk.DivSolidBackground +DivStroke = dk.DivStroke +DivStrokeStyleDashed = dk.DivStrokeStyleDashed +DivText = dk.DivText +DivTimer = dk.DivTimer +DivWrapContentSize = dk.DivWrapContentSize + + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +TOTAL_STEPS = 8 +POLL_INTERVAL_MS = 2000 # poll every 2 seconds +POLL_DURATION_MS = 120_000 # keep polling for up to 2 minutes +MESSAGES_ENDPOINT = "http://10.212.134.3:8003/new_messages" + + +def _make_action(log_id: str, url: str) -> DivAction: + return DivAction(log_id=log_id, url=url) + + +# --------------------------------------------------------------------------- +# Variables (8 steps) +# --------------------------------------------------------------------------- +def _build_variables() -> List[dk.DivVariable]: + variables: List[dk.DivVariable] = [ + BooleanVariable(name="is_running", value=False), + IntegerVariable(name="current_step", value=0), + StringVariable(name="status_text", value="Idle"), + ] + for i in range(1, TOTAL_STEPS + 1): + variables.append(BooleanVariable(name=f"step_{i}_visible", value=False)) + variables.append(StringVariable(name=f"step_{i}_text", value="")) + variables.extend( + [ + StringVariable(name="result_text", value=""), + BooleanVariable(name="show_result", value=False), + ] + ) + return variables + + +# --------------------------------------------------------------------------- +# Timers – single polling timer that fetches from the backend +# --------------------------------------------------------------------------- +def _build_timers() -> List[DivTimer]: + """Return a polling timer that hits the backend for new messages.""" + return [ + DivTimer( + id="poll_timer", + duration=POLL_DURATION_MS, + tick_interval=POLL_INTERVAL_MS, + tick_actions=[ + _make_action( + "poll_messages", + MESSAGES_ENDPOINT, + ), + ], + ), + ] + + +# --------------------------------------------------------------------------- +# Reusable step row +# --------------------------------------------------------------------------- +def _make_step_container( + step_number: int, + visible_var: str, + text_var: str, + margin_bottom: int | None = 8, +) -> DivContainer: + return DivContainer( + orientation=DivContainerOrientation.HORIZONTAL, + width=DivMatchParentSize(), + background=[DivSolidBackground(color="#1E293B")], + border=DivBorder( + corner_radius=8, + stroke=DivStroke(color="#334155", width=1), + ), + paddings=DivEdgeInsets(left=12, right=12, top=10, bottom=10), + margins=DivEdgeInsets(bottom=margin_bottom) if margin_bottom else None, + visibility=Expr(f"@{{{visible_var} ? 'visible' : 'gone'}}"), + items=[ + DivContainer( + width=DivFixedSize(value=24), + height=DivFixedSize(value=24), + background=[DivSolidBackground(color="#1E293B")], + border=DivBorder( + corner_radius=12, + stroke=DivStroke(color="#475569", width=1), + ), + items=[ + DivText( + text=str(step_number), + font_size=12, + text_color="#CBD5E1", + ) + ], + ), + DivText( + text=Expr(f"@{{{text_var}}}"), + font_size=14, + text_color="#F1F5F9", + margins=DivEdgeInsets(left=12), + width=DivMatchParentSize(), + ), + ], + ) + + +# --------------------------------------------------------------------------- +# Header +# --------------------------------------------------------------------------- +def _make_header() -> DivContainer: + return DivContainer( + orientation=DivContainerOrientation.VERTICAL, + width=DivMatchParentSize(), + margins=DivEdgeInsets(bottom=24), + items=[ + DivText( + text="Agent run progress", + font_size=24, + font_weight=dk.DivFontWeight.MEDIUM, + text_color="#F8FAFC", + ), + DivText( + text=( + "This page displays real-time progress of an AI agent. " + "Messages are fetched dynamically from the backend as the " + "agent works through its 8-step pipeline." + ), + font_size=14, + text_color="#94A3B8", + margins=DivEdgeInsets(top=8), + max_lines=3, + ), + ], + ) + + +# --------------------------------------------------------------------------- +# Start / Reset buttons +# --------------------------------------------------------------------------- +def _make_buttons() -> DivContainer: + start_actions = [ + _make_action( + "start_agent", + "div-action://set_variable?name=is_running&value=true", + ), + _make_action( + "set_status_running", + "div-action://set_variable?name=status_text&value=Running...", + ), + _make_action( + "start_polling", + "div-action://timer?id=poll_timer&action=start", + ), + ] + + reset_actions: List[DivAction] = [ + _make_action( + "stop_polling", + "div-action://timer?id=poll_timer&action=stop", + ), + _make_action( + "reset_running", + "div-action://set_variable?name=is_running&value=false", + ), + _make_action( + "reset_status", + "div-action://set_variable?name=status_text&value=Idle", + ), + ] + for i in range(1, TOTAL_STEPS + 1): + reset_actions.append( + _make_action( + f"reset_step{i}_vis", + f"div-action://set_variable?name=step_{i}_visible&value=false", + ) + ) + reset_actions.append( + _make_action( + f"reset_step{i}_text", + f"div-action://set_variable?name=step_{i}_text&value=", + ) + ) + reset_actions.extend( + [ + _make_action( + "reset_show_result", + "div-action://set_variable?name=show_result&value=false", + ), + _make_action( + "reset_result_text", + "div-action://set_variable?name=result_text&value=", + ), + ] + ) + + return DivContainer( + orientation=DivContainerOrientation.HORIZONTAL, + width=DivMatchParentSize(), + margins=DivEdgeInsets(bottom=24), + items=[ + DivContainer( + orientation=DivContainerOrientation.HORIZONTAL, + width=DivWrapContentSize(), + background=[DivSolidBackground(color="#10B981")], + border=DivBorder(corner_radius=8), + paddings=DivEdgeInsets(left=16, right=16, top=8, bottom=8), + actions=start_actions, + items=[ + DivText( + text="▶ Start agent", + font_size=14, + font_weight=dk.DivFontWeight.MEDIUM, + text_color="#020617", + ) + ], + ), + DivContainer( + orientation=DivContainerOrientation.HORIZONTAL, + width=DivWrapContentSize(), + background=[DivSolidBackground(color="#1E293B")], + border=DivBorder( + corner_radius=8, + stroke=DivStroke(color="#334155", width=1), + ), + paddings=DivEdgeInsets(left=12, right=12, top=8, bottom=8), + margins=DivEdgeInsets(left=8), + actions=reset_actions, + items=[ + DivText( + text="🔄 Reset", + font_size=12, + text_color="#E2E8F0", + ) + ], + ), + ], + ) + + +# --------------------------------------------------------------------------- +# Agent activity section (8 step rows + empty-state placeholder) +# --------------------------------------------------------------------------- +def _make_agent_activity_section() -> DivContainer: + header_row = DivContainer( + orientation=DivContainerOrientation.HORIZONTAL, + width=DivMatchParentSize(), + margins=DivEdgeInsets(bottom=16), + items=[ + DivText( + text="🕐 Agent activity", + font_size=14, + font_weight=dk.DivFontWeight.MEDIUM, + text_color="#E2E8F0", + width=DivWrapContentSize(), + ), + DivContainer( + width=DivMatchParentSize(), + items=[ + DivText( + text=Expr("@{status_text}"), + font_size=12, + text_color="#94A3B8", + ) + ], + ), + ], + ) + + # Build 8 step containers + step_items: List[DivContainer] = [] + for i in range(1, TOTAL_STEPS + 1): + mb = 8 if i < TOTAL_STEPS else None + step_items.append( + _make_step_container(i, f"step_{i}_visible", f"step_{i}_text", mb) + ) + + # Empty-state placeholder (shown when no steps are visible yet) + empty_state = DivContainer( + orientation=DivContainerOrientation.HORIZONTAL, + width=DivMatchParentSize(), + background=[DivSolidBackground(color="#020617")], + border=DivBorder( + corner_radius=8, + stroke=DivStroke( + color="#1E293B", + width=1, + style=DivStrokeStyleDashed(), + ), + ), + paddings=DivEdgeInsets(left=16, right=16, top=12, bottom=12), + visibility=Expr("@{step_1_visible ? 'gone' : 'visible'}"), + items=[ + DivIndicator( + width=DivFixedSize(value=14), + height=DivFixedSize(value=14), + active_item_color="#38BDF8", + inactive_item_color="#475569", + shape=DivCircleShape(), + minimum_item_size=0.4, + active_item_size=1.0, + visibility=Expr("@{is_running ? 'visible' : 'gone'}"), + ), + DivText( + text="Waiting for the agent to report its first step…", + font_size=12, + text_color="#64748B", + margins=DivEdgeInsets(left=8), + visibility=Expr("@{is_running ? 'visible' : 'gone'}"), + ), + DivText( + text=( + "Start the agent to see progress messages appear " + "here as they come in." + ), + font_size=12, + text_color="#64748B", + visibility=Expr("@{is_running ? 'gone' : 'visible'}"), + ), + ], + ) + step_items.append(empty_state) + + steps_list = DivContainer( + orientation=DivContainerOrientation.VERTICAL, + width=DivMatchParentSize(), + items=step_items, + ) + + return DivContainer( + orientation=DivContainerOrientation.VERTICAL, + width=DivMatchParentSize(), + background=[DivSolidBackground(color="#0F172A")], + border=DivBorder( + corner_radius=12, + stroke=DivStroke(color="#1E293B", width=1), + ), + paddings=DivEdgeInsets(left=20, right=20, top=20, bottom=20), + margins=DivEdgeInsets(bottom=24), + items=[header_row, steps_list], + ) + + +# --------------------------------------------------------------------------- +# Agent output section +# --------------------------------------------------------------------------- +def _make_agent_output_section() -> DivContainer: + running_row = DivContainer( + orientation=DivContainerOrientation.HORIZONTAL, + visibility=Expr("@{is_running ? 'visible' : 'gone'}"), + items=[ + DivIndicator( + width=DivFixedSize(value=16), + height=DivFixedSize(value=16), + active_item_color="#4ADE80", + inactive_item_color="#475569", + shape=DivCircleShape(), + ), + DivText( + text="Agent is still working on the answer…", + font_size=14, + text_color="#94A3B8", + margins=DivEdgeInsets(left=8), + ), + ], + ) + idle_text = DivText( + text='Click "Start agent" to see the simulated progress and final result here.', + font_size=14, + text_color="#64748B", + visibility=Expr("@{is_running || show_result ? 'gone' : 'visible'}"), + ) + result_row = DivContainer( + orientation=DivContainerOrientation.HORIZONTAL, + visibility=Expr("@{show_result ? 'visible' : 'gone'}"), + items=[ + DivText( + text="✓", + font_size=16, + text_color="#10B981", + ), + DivText( + text=Expr("@{result_text}"), + font_size=14, + text_color="#CBD5E1", + margins=DivEdgeInsets(left=8), + ), + ], + ) + return DivContainer( + orientation=DivContainerOrientation.VERTICAL, + width=DivMatchParentSize(), + items=[running_row, idle_text, result_row], + ) + + +# --------------------------------------------------------------------------- +# Card builder +# --------------------------------------------------------------------------- +def build_agent_progress_card() -> Dict[str, Any]: + """Build DivKit card with 8-step agent progress that polls the backend.""" + root = DivContainer( + orientation=DivContainerOrientation.VERTICAL, + width=DivMatchParentSize(), + height=DivWrapContentSize(), + background=[DivSolidBackground(color="#020617")], + paddings=DivEdgeInsets(left=24, right=24, top=24, bottom=24), + items=[ + _make_header(), + _make_buttons(), + _make_agent_activity_section(), + _make_agent_output_section(), + ], + ) + + card: Dict[str, Any] = { + "card": { + "log_id": "agent_progress", + "variables": [v.dict() for v in _build_variables()], + "states": [ + { + "state_id": 0, + "div": root.dict(), + }, + ], + "timers": [timer.dict() for timer in _build_timers()], + } + } + + # Patch indicator items_count for the empty-state indicator. + activity_container = card["card"]["states"][0]["div"]["items"][2] + empty_state = activity_container["items"][1]["items"][-1] + empty_indicator = empty_state["items"][0] + empty_indicator["items_count"] = TOTAL_STEPS + + return card + + +# --------------------------------------------------------------------------- +# Helper: build a step-row dict (used by the /new_messages endpoint to +# create DivKit patches without importing pydivkit on every request). +# --------------------------------------------------------------------------- +def make_step_row_dict(step_number: int, text: str) -> Dict[str, Any]: + """Return a raw DivKit JSON dict for a single visible step row.""" + return { + "type": "container", + "orientation": "horizontal", + "width": {"type": "match_parent"}, + "background": [{"type": "solid", "color": "#1E293B"}], + "border": { + "corner_radius": 8, + "stroke": {"color": "#334155", "width": 1.0}, + }, + "paddings": {"left": 12, "right": 12, "top": 10, "bottom": 10}, + "margins": {"bottom": 8} if step_number < TOTAL_STEPS else None, + "visibility": "visible", + "items": [ + { + "type": "container", + "width": {"type": "fixed", "value": 24}, + "height": {"type": "fixed", "value": 24}, + "background": [{"type": "solid", "color": "#1E293B"}], + "border": { + "corner_radius": 12, + "stroke": {"color": "#475569", "width": 1.0}, + }, + "items": [ + { + "type": "text", + "text": str(step_number), + "font_size": 12, + "text_color": "#CBD5E1", + } + ], + }, + { + "type": "text", + "text": text, + "font_size": 14, + "text_color": "#F1F5F9", + "margins": {"left": 12}, + "width": {"type": "match_parent"}, + }, + ], + } + + +def save_agent_progress(path: str = "agent_progress.json") -> None: + """Save the built card to JSON file.""" + card = build_agent_progress_card() + with open(path, "w", encoding="utf-8") as f: + json.dump(card, f, indent=2, ensure_ascii=False) + + +if __name__ == "__main__": + save_agent_progress() diff --git a/functions_to_format/functions/__init__.py b/functions_to_format/functions/__init__.py index e5500ce..d35e438 100644 --- a/functions_to_format/functions/__init__.py +++ b/functions_to_format/functions/__init__.py @@ -1,69 +1,7 @@ -from .functions import chatbot_answer, unauthorized_response, build_contacts_list -from .balance import get_balance, build_balance_ui, get_home_balances -from .payment import ( - get_receiver_id_by_receiver_phone_number, - get_categories, - get_fields_of_supplier, - get_suppliers_by_category, - get_number_by_receiver_name, - send_money_to_someone_via_card, - pay_for_home_utility, - get_home_utility_suppliers, - get_receiver_by_card, - build_receiver_by_card_ui, - build_send_money_ui, -) -from .contact import build_contact_widget, get_contact -from .news import build_news_widget, get_news -from .notification import build_notifications_widget, get_notifications -from .products import build_products_list_widget, get_products, search_products -from .weather import build_weather_widget, get_weather_info -from .buttons import build_buttons_row -from .start_page import start_page_widget -from .human_approval import human_approval_requests +from .functions import functions_mapper, sdui_functions_map -sdui_functions_map = { - "notifications": build_notifications_widget, - "get_balance": build_balance_ui, - "contact": build_contact_widget, - "news": build_news_widget, - "products": build_products_list_widget, - "weather": build_weather_widget, - "get_receiver_id_by_reciver_phone_number": None, - "get_categories": None, - "get_fields_of_supplier": None, - "get_suppliers_by_category": None, - "chatbot_answer": None, - "text_widget": None, - "buttons_widget": build_buttons_row, - "receiver_by_card": build_receiver_by_card_ui, - "send_money": build_send_money_ui, -} -functions_mapper = { - "get_balance": get_balance, - "get_weather_info": get_weather_info, - "get_news": get_news, - "get_products": get_products, - "search_products": search_products, - "get_notifications": get_notifications, - "get_contact": get_contact, - "chatbot_answer": chatbot_answer, - "unauthorized_response": unauthorized_response, - "get_number_by_receiver_name": get_number_by_receiver_name, - "get_receiver_id_by_reciver_phone_number": get_receiver_id_by_receiver_phone_number, - "get_receiver_id_by_receiver_phone_number": get_receiver_id_by_receiver_phone_number, - "get_categories": get_categories, - "get_fields_of_supplier": get_fields_of_supplier, - "get_suppliers_by_category": get_suppliers_by_category, - "start_page_widget": start_page_widget, - "send_money_to_someone_via_card": send_money_to_someone_via_card, - "get_home_balances": get_home_balances, - "build_contacts_list": build_contacts_list, - "pay_for_home_utility": pay_for_home_utility, - "get_home_utility_suppliers": get_home_utility_suppliers, - "human_approval": human_approval_requests, - "human_approval_request": human_approval_requests, - "get_receiver_by_card": get_receiver_by_card, - "receiver_by_card": get_receiver_by_card, -} +__all__ = [ + "functions_mapper", + "sdui_functions_map", +] diff --git a/functions_to_format/functions/activity_report.py b/functions_to_format/functions/activity_report.py new file mode 100644 index 0000000..1a62403 --- /dev/null +++ b/functions_to_format/functions/activity_report.py @@ -0,0 +1,295 @@ +import pydivkit as dv +import json +from pydantic import BaseModel +from typing import Any, Dict, List +from pydivkit.core import Expr +from .general import Widget, add_ui_to_widget, WidgetInput +from .general.utils import save_builder_output +from models.build import BuildOutput +from functions_to_format.functions.general.const_values import LanguageOptions +from conf import logger +from models.context import Context, LoggerContext +import structlog + + +# Backend output schemas for activity reports +class FunctionCallBackendOutput(BaseModel): + """Backend output for function_call activity report.""" + + function_name: str + arguments: Dict[str, Any] + + +class FunctionResponseBackendOutput(BaseModel): + """Backend output for function_response activity report.""" + + function_name: str + response: Dict[str, Any] + + +class ActivityIndicatorWidget(BaseModel): + message: str + + +# Reasoning-like design: gray trigger block, muted content +MESSAGE_BLOCK_BG = "#374151" +MESSAGE_TEXT_COLOR = "#F8FAFC" +CONTENT_TITLE_COLOR = "#94A3B8" +CONTENT_DETAIL_COLOR = "#64748B" +CHEVRON_COLOR = "#94A3B8" +# Content block: subtle card so detail feels grouped +CONTENT_BLOCK_BG = "#1E293B" +CONTENT_BLOCK_BORDER = "#334155" +CONTENT_PADDING = 14 +TRIGGER_PADDING_V = 12 +TRIGGER_PADDING_H = 16 +TRIGGER_RADIUS = 10 +CONTENT_RADIUS = 8 +SPACING_ABOVE_CONTENT = 14 +GAP_MESSAGE_CHEVRON = 12 + + +def _message_glow_text(message: str) -> dv.DivText: + """Message text with glow animation: soft shadow + fade-in transition.""" + shadow_offset = dv.DivPoint( + x=dv.DivDimension(value=0), + y=dv.DivDimension(value=0), + ) + text_shadow = dv.DivShadow( + offset=shadow_offset, + color="#7DD3FC", + blur=10, + alpha=0.7, + ) + transition_in = dv.DivFadeTransition( + duration=600, + alpha=0.2, + interpolator=dv.DivAnimationInterpolator.EASE_OUT, + ) + return dv.DivText( + text=message, + font_size=14, + text_color=MESSAGE_TEXT_COLOR, + text_shadow=text_shadow, + transition_in=transition_in, + ) + + +def _trigger_row( + message: str, + chevron: str, + state_id: str, + target_state: str, + log_id: str, +) -> dv.DivContainer: + """Trigger row: message + chevron (like ReasoningTrigger). Tapping toggles state.""" + set_state_url = f"div-action://set_state?state_id=0/{state_id}/{target_state}" + return dv.DivContainer( + orientation=dv.DivContainerOrientation.HORIZONTAL, + background=[dv.DivSolidBackground(color=MESSAGE_BLOCK_BG)], + paddings=dv.DivEdgeInsets( + left=TRIGGER_PADDING_H, + right=TRIGGER_PADDING_H, + top=TRIGGER_PADDING_V, + bottom=TRIGGER_PADDING_V, + ), + border=dv.DivBorder( + corner_radius=TRIGGER_RADIUS, + stroke=dv.DivStroke(color="#4B5563", width=1), + ), + alignment_vertical=dv.DivAlignmentVertical.CENTER, + items=[ + dv.DivContainer( + orientation=dv.DivContainerOrientation.VERTICAL, + width=dv.DivMatchParentSize(weight=1), + margins=dv.DivEdgeInsets(right=GAP_MESSAGE_CHEVRON), + items=[_message_glow_text(message)], + ), + dv.DivText( + text=chevron, + font_size=16, + text_color=CHEVRON_COLOR, + ), + ], + action=dv.DivAction(log_id=log_id, url=set_state_url), + ) + + +def _build_activity_message_holder( + message: str, + title: str, + detail_json: str, + log_id: str, + action_url: str = "div-action://activity-report", +): + """Reasoning-like collapsible: trigger row (message + chevron) + expandable content (title + detail).""" + state_id = f"activity_{log_id}" + + # Collapsed: only trigger row with ▼ (expand) + collapsed_trigger = _trigger_row( + message=message, + chevron="▼", + state_id=state_id, + target_state="expanded", + log_id=log_id, + ) + + # Expanded: trigger row with ▲ (collapse) + content below in a card + content_inner = dv.DivContainer( + orientation=dv.DivContainerOrientation.VERTICAL, + items=[ + dv.DivText( + text=title, + font_size=13, + text_color=CONTENT_TITLE_COLOR, + font_weight=dv.DivFontWeight.MEDIUM, + margins=dv.DivEdgeInsets(bottom=8), + ), + dv.DivText( + text=detail_json, + font_size=11, + text_color=CONTENT_DETAIL_COLOR, + line_height=18, + ), + ], + ) + content_block = dv.DivContainer( + orientation=dv.DivContainerOrientation.VERTICAL, + margins=dv.DivEdgeInsets(top=SPACING_ABOVE_CONTENT), + paddings=dv.DivEdgeInsets( + left=CONTENT_PADDING, + right=CONTENT_PADDING, + top=CONTENT_PADDING, + bottom=CONTENT_PADDING, + ), + background=[dv.DivSolidBackground(color=CONTENT_BLOCK_BG)], + border=dv.DivBorder( + corner_radius=CONTENT_RADIUS, + stroke=dv.DivStroke(color=CONTENT_BLOCK_BORDER, width=1), + ), + transition_in=dv.DivFadeTransition(duration=200, alpha=0.5), + items=[content_inner], + ) + expanded_trigger = _trigger_row( + message=message, + chevron="▲", + state_id=state_id, + target_state="collapsed", + log_id=log_id, + ) + expanded_div = dv.DivContainer( + orientation=dv.DivContainerOrientation.VERTICAL, + items=[ + expanded_trigger, + content_block, + ], + ) + + state = dv.DivState( + id=state_id, + default_state_id="expanded", + states=[ + dv.DivStateState(state_id="collapsed", div=collapsed_trigger), + dv.DivStateState(state_id="expanded", div=expanded_div), + ], + ) + wrapper = dv.DivContainer( + orientation=dv.DivContainerOrientation.VERTICAL, + margins=dv.DivEdgeInsets(left=12, right=12, top=8, bottom=8), + items=[state], + ) + return dv.make_div(wrapper) + + +def build_function_call_activity_widget( + message: str, + function_name: str, + arguments: Dict[str, Any], +): + """Builder for function_call activity: shows message and call details (function_name, arguments).""" + title = f"Calling: {function_name}" + detail_json = json.dumps(arguments, ensure_ascii=False, default=str)[:500] + if len(detail_json) >= 500: + detail_json += "..." + return _build_activity_message_holder( + message=message, + title=title, + detail_json=detail_json, + log_id="function_call_activity_report", + action_url="div-action://function-call-activity-report", + ) + + +def build_function_response_activity_widget( + message: str, + function_name: str, + response: Dict[str, Any], +): + """Builder for function_response activity: shows message and response (function_name, response).""" + title = f"Response from: {function_name}" + detail_json = json.dumps(response, ensure_ascii=False, default=str)[:500] + if len(detail_json) >= 500: + detail_json += "..." + return _build_activity_message_holder( + message=message, + title=title, + detail_json=detail_json, + log_id="function_response_activity_report", + action_url="div-action://function-response-activity-report", + ) + + +def build_activity_indicator_widget(message: str) -> ActivityIndicatorWidget: + container = dv.DivContainer( + id="activity_report", + orientation=dv.DivContainerOrientation.VERTICAL, + items=[ + dv.DivText(text=message, font_size=14, text_color="#F1F5F9"), + ], + action=dv.DivAction( + log_id="activity_report", + url="div-action://activity-report", + payload={ + "message": message, + "link": "http://10.212.134.3:8003/new_messages?idx=12341234", + }, + ), + ) + + return dv.make_div(container) + + +def activity_indicator(context: Context) -> BuildOutput: + # Extract values from context + llm_output = context.llm_output + backend_output = context.backend_output + version = context.version + language = context.language + chat_id = context.logger_context.chat_id + api_key = context.api_key + logger = context.logger_context.logger + + inp = {} + + activity_indicator_widget = Widget( + name="activity_indicator_widget", + type="activity_indicator_widget", + order=1, + layout="vertical", + fields=["activity_indicator"], + ) + inp[build_activity_indicator_widget] = WidgetInput( + widget=activity_indicator_widget, + args={"message": llm_output}, + ) + widgets = add_ui_to_widget( + widget_inputs=inp, + version=version, + ) + output = BuildOutput( + widgets_count=len(widgets), + widgets=[widget.model_dump(exclude_none=True) for widget in widgets], + ) + + save_builder_output(context, output) + return output diff --git a/functions_to_format/functions/activity_report_events.py b/functions_to_format/functions/activity_report_events.py new file mode 100644 index 0000000..1d1e72a --- /dev/null +++ b/functions_to_format/functions/activity_report_events.py @@ -0,0 +1,85 @@ +from .general import Widget, add_ui_to_widget, WidgetInput +from .general.utils import save_builder_output +from models.build import BuildOutput +from models.context import Context + +from .activity_report import ( + FunctionCallBackendOutput, + FunctionResponseBackendOutput, + build_function_call_activity_widget, + build_function_response_activity_widget, +) + + +def function_call_activity_record(context: Context) -> BuildOutput: + """Build UI for function_call activity: message from llm_output, function_name + arguments from backend_output.""" + llm_output = context.llm_output + backend_output = context.backend_output + version = context.version + + data = FunctionCallBackendOutput( + function_name=backend_output.get("function_name", ""), + arguments=backend_output.get("arguments", {}), + ) + + activity_widget = Widget( + name="function_call_activity_widget", + type="function_call_activity_widget", + order=1, + layout="vertical", + fields=["function_call_activity"], + ) + inp = { + build_function_call_activity_widget: WidgetInput( + widget=activity_widget, + args={ + "message": llm_output, + "function_name": data.function_name, + "arguments": data.arguments, + }, + ) + } + widgets = add_ui_to_widget(widget_inputs=inp, version=version) + output = BuildOutput( + widgets_count=len(widgets), + widgets=[w.model_dump(exclude_none=True) for w in widgets], + ) + save_builder_output(context, output) + return output + + +def function_response_activity_record(context: Context) -> BuildOutput: + """Build UI for function_response activity: message from llm_output, function_name + response from backend_output.""" + llm_output = context.llm_output + backend_output = context.backend_output + version = context.version + + data = FunctionResponseBackendOutput( + function_name=backend_output.get("function_name", ""), + response=backend_output.get("response", {}), + ) + + activity_widget = Widget( + name="function_response_activity_widget", + type="function_response_activity_widget", + order=1, + layout="vertical", + fields=["function_response_activity"], + ) + inp = { + build_function_response_activity_widget: WidgetInput( + widget=activity_widget, + args={ + "message": llm_output, + "function_name": data.function_name, + "response": data.response, + }, + ) + } + widgets = add_ui_to_widget(widget_inputs=inp, version=version) + output = BuildOutput( + widgets_count=len(widgets), + widgets=[w.model_dump(exclude_none=True) for w in widgets], + ) + save_builder_output(context, output) + return output diff --git a/functions_to_format/functions/chatbot_answer.py b/functions_to_format/functions/chatbot_answer.py new file mode 100644 index 0000000..e7cab12 --- /dev/null +++ b/functions_to_format/functions/chatbot_answer.py @@ -0,0 +1,341 @@ +import json +from typing import Any, Dict, List +from .general import ( + add_ui_to_widget, + Widget, + TextWidget, + ButtonsWidget, + build_buttons_row, + build_text_widget, + WidgetInput, +) +from .general.utils import save_builder_output +import pydivkit as dv +from pydivkit.core import Expr +from models.build import BuildOutput +from pydantic import BaseModel +from .general.const_values import WidgetMargins, LanguageOptions +from models.context import Context + + +# Feedback texts for contact actions +CONTACT_FEEDBACK_TEXTS = { + LanguageOptions.RUSSIAN: { + "contact_selected": "Контакт выбран", + "contact_error": "Ошибка выбора контакта", + }, + LanguageOptions.ENGLISH: { + "contact_selected": "Contact selected", + "contact_error": "Contact selection error", + }, + LanguageOptions.UZBEK: { + "contact_selected": "Kontakt tanlandi", + "contact_error": "Kontaktni tanlashda xatolik", + }, +} + + +def chatbot_answer(context: Context) -> BuildOutput: + # Janis Rubins: logic unchanged, just returns text_widget schema and llm_output as data + + # Extract values from context + llm_output = context.llm_output + backend_output = context.backend_output + version = context.version + language = context.language + chat_id = context.logger_context.chat_id + api_key = context.api_key + logger = context.logger_context.logger + + text_widget = TextWidget( + order=1, + values=[{"text": llm_output}], + ) + + widgets = add_ui_to_widget( + { + build_text_widget: WidgetInput( + widget=text_widget, + args={"text": llm_output}, + ), + }, + version, + ) + + output = BuildOutput( + widgets_count=1, + widgets=[widget.model_dump(exclude_none=True) for widget in widgets], + ) + save_builder_output(context, output) + return output + + +def unauthorized_response(context: Context): + # Extract values from context + llm_output = context.llm_output + backend_output = context.backend_output + version = context.version + language = context.language + chat_id = context.logger_context.chat_id + api_key = context.api_key + logger = context.logger_context.logger + + return { + "data": llm_output, + } + + +class Contact(BaseModel): + first_name: str + last_name: str + phone: str + + +def build_contacts_list_widget( + contacts: list[Contact], + language: LanguageOptions = LanguageOptions.RUSSIAN, +) -> Dict[str, Any]: + """ + Build a contacts list widget with feedback handling for contact selection. + + Args: + contacts: List of contact objects + language: Language for localization + + Returns: + DivKit JSON for the contacts list widget + """ + feedback_texts = CONTACT_FEEDBACK_TEXTS.get( + language, CONTACT_FEEDBACK_TEXTS[LanguageOptions.ENGLISH] + ) + + items: List[dv.Div] = [ + dv.DivText( + text="Contacts List", + font_family="Manrope", + font_size=18, + font_weight=dv.DivFontWeight.BOLD, + text_color="#111133", + margins=dv.DivEdgeInsets(bottom=16), + ) + ] + + for idx, contact in enumerate(contacts): + contact_cell = dv.DivContainer( + orientation=dv.DivContainerOrientation.VERTICAL, + paddings=dv.DivEdgeInsets(left=16, right=16, top=8, bottom=8), + border=dv.DivBorder( + stroke=dv.DivStroke(color="#E0E0E0"), + ), + items=[ + dv.DivText( + text=f"{contact.first_name} {contact.last_name}", + font_family="Manrope", + font_size=16, + font_weight=dv.DivFontWeight.MEDIUM, + text_color="#111133", + line_height=20, + letter_spacing=0, + ), + dv.DivText( + text=contact.phone, + font_family="Manrope", + font_size=14, + font_weight=dv.DivFontWeight.REGULAR, + text_color="#808080", + line_height=20, + letter_spacing=0, + ), + ], + actions=[ + # Main selection action + dv.DivAction( + log_id=f"send_contact_{contact.phone.replace('+', '').replace(' ', '')}", + url="divkit://send_contact", + payload={ + "first_name": contact.first_name, + "last_name": contact.last_name, + "phone": contact.phone, + }, + ), + # Success feedback action + dv.DivAction( + log_id=f"send_contact_{idx}_success", + url="div-action://set_variable?name=contact_selection_success&value=1", + ), + ], + ) + items.append(contact_cell) + + # Success feedback container + success_container = dv.DivContainer( + id="contact-selection-success", + orientation=dv.DivContainerOrientation.HORIZONTAL, + visibility=Expr("@{contact_selection_success == 1 ? 'visible' : 'gone'}"), + alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + width=dv.DivMatchParentSize(), + margins=dv.DivEdgeInsets(top=12, bottom=4), + paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), + background=[dv.DivSolidBackground(color="#ECFDF5")], + border=dv.DivBorder( + corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) + ), + items=[ + dv.DivText( + text="✅", + font_size=14, + margins=dv.DivEdgeInsets(right=8), + ), + dv.DivText( + text=feedback_texts["contact_selected"], + font_size=13, + text_color="#065F46", + font_weight=dv.DivFontWeight.MEDIUM, + width=dv.DivMatchParentSize(weight=1), + ), + dv.DivText( + text="✕", + font_size=16, + text_color="#065F46", + font_weight=dv.DivFontWeight.BOLD, + paddings=dv.DivEdgeInsets(left=8), + actions=[ + dv.DivAction( + log_id="dismiss-contact-selection-success", + url="div-action://set_variable?name=contact_selection_success&value=0", + ) + ], + ), + ], + ) + + # Error feedback container + error_container = dv.DivContainer( + id="contact-selection-error", + orientation=dv.DivContainerOrientation.HORIZONTAL, + visibility=Expr("@{contact_selection_error == 1 ? 'visible' : 'gone'}"), + alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, + width=dv.DivMatchParentSize(), + margins=dv.DivEdgeInsets(top=12, bottom=4), + paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), + background=[dv.DivSolidBackground(color="#FEF2F2")], + border=dv.DivBorder( + corner_radius=10, stroke=dv.DivStroke(color="#FECACA", width=1) + ), + items=[ + dv.DivText( + text="⚠️", + font_size=14, + margins=dv.DivEdgeInsets(right=8), + ), + dv.DivText( + text=feedback_texts["contact_error"], + font_size=13, + text_color="#B91C1C", + font_weight=dv.DivFontWeight.MEDIUM, + width=dv.DivMatchParentSize(weight=1), + ), + dv.DivText( + text="✕", + font_size=16, + text_color="#B91C1C", + font_weight=dv.DivFontWeight.BOLD, + paddings=dv.DivEdgeInsets(left=8), + actions=[ + dv.DivAction( + log_id="dismiss-contact-selection-error", + url="div-action://set_variable?name=contact_selection_error&value=0", + ) + ], + ), + ], + ) + + items.append(success_container) + items.append(error_container) + + container = dv.DivContainer( + orientation=dv.DivContainerOrientation.VERTICAL, + items=items, + variables=[ + dv.IntegerVariable(name="contact_selection_success", value=0), + dv.IntegerVariable(name="contact_selection_error", value=0), + ], + margins=dv.DivEdgeInsets( + top=WidgetMargins.TOP.value, + left=WidgetMargins.LEFT.value, + right=WidgetMargins.RIGHT.value, + bottom=WidgetMargins.BOTTOM.value, + ), + paddings=dv.DivEdgeInsets(left=16, right=16, top=8, bottom=8), + ) + container = dv.make_div(container) + with open("logs/json/build_contacts.json", "w") as f: + json.dump(container, f, indent=4) + return container + + +def build_contacts_list(context: Context) -> BuildOutput: + # Extract values from context + llm_output = context.llm_output + backend_output = context.backend_output + version = context.version + language = context.language + chat_id = context.logger_context.chat_id + api_key = context.api_key + logger = context.logger_context.logger + + contacts_list: list[Contact] = [] + + contact_widget = Widget( + order=1, + name="contacts_list", + type="contacts_list", + layout="horizontal", + fields=["contacts"], + values=[{"contacts": contacts_list}], + ) + # text_widget = TextWidget( + # order=1, + # values=[{"text": llm_output}], + # ) + buttons = ButtonsWidget( + order=2, + values=[{"text": "cancel"}], + ) + + for contact in backend_output: + contacts_list.append( + Contact( + first_name=contact["first_name"], + last_name=contact["last_name"], + phone=contact["phone"], + ) + ) + + widgets = add_ui_to_widget( + { + build_contacts_list_widget: WidgetInput( + widget=contact_widget, + args={ + "contacts": contacts_list, + }, + ), + build_buttons_row: WidgetInput( + widget=buttons, + args={"button_texts": ["cancel"], "language": language}, + ), + # build_text_widget: WidgetInput( + # widget=text_widget, + # args={"text": llm_output}, + # ), + }, + version, + ) + + output = BuildOutput( + widgets_count=1, + widgets=[widget.model_dump(exclude_none=True) for widget in widgets], + ) + save_builder_output(context, output) + return output diff --git a/functions_to_format/functions/functions.py b/functions_to_format/functions/functions.py index 65d0d80..78bde7e 100644 --- a/functions_to_format/functions/functions.py +++ b/functions_to_format/functions/functions.py @@ -1,349 +1,75 @@ -import json -import re -import hashlib -from functools import lru_cache -from typing import Any, Dict, List -from conf import logger -from .general import ( - add_ui_to_widget, - Widget, - TextWidget, - ButtonsWidget, - build_buttons_row, - build_text_widget, - WidgetInput, - create_feedback_variables, - create_success_actions, - create_failure_actions, - create_success_container, - create_error_container, +from .chatbot_answer import chatbot_answer, unauthorized_response, build_contacts_list +from .balance import get_balance, build_balance_ui, get_home_balances +from .payment import ( + get_receiver_id_by_receiver_phone_number, + get_categories, + get_fields_of_supplier, + get_suppliers_by_category, + get_number_by_receiver_name, + send_money_to_someone_via_card, + pay_for_home_utility, + get_home_utility_suppliers, + get_receiver_by_card, + build_receiver_by_card_ui, + build_send_money_ui, +) +from .contact import build_contact_widget, get_contact +from .news import build_news_widget, get_news +from .notification import build_notifications_widget, get_notifications +from .products import build_products_list_widget, get_products, search_products +from .weather import build_weather_widget, get_weather_info +from .buttons import build_buttons_row +from .start_page import start_page_widget +from .human_approval import human_approval_requests +from .activity_report_events import ( + function_call_activity_record, + function_response_activity_record, ) -from .general.utils import save_builder_output -import pydivkit as dv -from pydivkit.core import Expr -from models.build import BuildOutput -from pydantic import BaseModel -from .general.const_values import WidgetMargins, LanguageOptions -import structlog -from models.context import Context -# Feedback texts for contact actions -CONTACT_FEEDBACK_TEXTS = { - LanguageOptions.RUSSIAN: { - "contact_selected": "Контакт выбран", - "contact_error": "Ошибка выбора контакта", - }, - LanguageOptions.ENGLISH: { - "contact_selected": "Contact selected", - "contact_error": "Contact selection error", - }, - LanguageOptions.UZBEK: { - "contact_selected": "Kontakt tanlandi", - "contact_error": "Kontaktni tanlashda xatolik", - }, +sdui_functions_map = { + "notifications": build_notifications_widget, + "get_balance": build_balance_ui, + "contact": build_contact_widget, + "news": build_news_widget, + "products": build_products_list_widget, + "weather": build_weather_widget, + "get_receiver_id_by_reciver_phone_number": None, + "get_categories": None, + "get_fields_of_supplier": None, + "get_suppliers_by_category": None, + "chatbot_answer": None, + "text_widget": None, + "buttons_widget": build_buttons_row, + "receiver_by_card": build_receiver_by_card_ui, + "send_money": build_send_money_ui, +} +functions_mapper = { + "get_balance": get_balance, + "get_weather_info": get_weather_info, + "get_news": get_news, + "get_products": get_products, + "search_products": search_products, + "get_notifications": get_notifications, + "get_contact": get_contact, + "chatbot_answer": chatbot_answer, + "unauthorized_response": unauthorized_response, + "get_number_by_receiver_name": get_number_by_receiver_name, + "get_receiver_id_by_reciver_phone_number": get_receiver_id_by_receiver_phone_number, + "get_receiver_id_by_receiver_phone_number": get_receiver_id_by_receiver_phone_number, + "get_categories": get_categories, + "get_fields_of_supplier": get_fields_of_supplier, + "get_suppliers_by_category": get_suppliers_by_category, + "start_page_widget": start_page_widget, + "send_money_to_someone_via_card": send_money_to_someone_via_card, + "get_home_balances": get_home_balances, + "build_contacts_list": build_contacts_list, + "pay_for_home_utility": pay_for_home_utility, + "get_home_utility_suppliers": get_home_utility_suppliers, + "human_approval": human_approval_requests, + "human_approval_request": human_approval_requests, + "get_receiver_by_card": get_receiver_by_card, + "receiver_by_card": get_receiver_by_card, + "function_call_activity_record": function_call_activity_record, + "function_response_activity_record": function_response_activity_record, } - - -def chatbot_answer(context: Context) -> BuildOutput: - # Janis Rubins: logic unchanged, just returns text_widget schema and llm_output as data - - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - text_widget = TextWidget( - order=1, - values=[{"text": llm_output}], - ) - - widgets = add_ui_to_widget( - { - build_text_widget: WidgetInput( - widget=text_widget, - args={"text": llm_output}, - ), - }, - version, - ) - - output = BuildOutput( - widgets_count=1, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output - - -def unauthorized_response(context: Context): - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - return { - "data": llm_output, - } - - -class Contact(BaseModel): - first_name: str - last_name: str - phone: str - - -def build_contacts_list_widget( - contacts: list[Contact], - language: LanguageOptions = LanguageOptions.RUSSIAN, -) -> Dict[str, Any]: - """ - Build a contacts list widget with feedback handling for contact selection. - - Args: - contacts: List of contact objects - language: Language for localization - - Returns: - DivKit JSON for the contacts list widget - """ - feedback_texts = CONTACT_FEEDBACK_TEXTS.get(language, CONTACT_FEEDBACK_TEXTS[LanguageOptions.ENGLISH]) - - items: List[dv.Div] = [ - dv.DivText( - text="Contacts List", - font_family="Manrope", - font_size=18, - font_weight=dv.DivFontWeight.BOLD, - text_color="#111133", - margins=dv.DivEdgeInsets(bottom=16), - ) - ] - - for idx, contact in enumerate(contacts): - contact_cell = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - paddings=dv.DivEdgeInsets(left=16, right=16, top=8, bottom=8), - border=dv.DivBorder( - stroke=dv.DivStroke(color="#E0E0E0"), - ), - items=[ - dv.DivText( - text=f"{contact.first_name} {contact.last_name}", - font_family="Manrope", - font_size=16, - font_weight=dv.DivFontWeight.MEDIUM, - text_color="#111133", - line_height=20, - letter_spacing=0, - ), - dv.DivText( - text=contact.phone, - font_family="Manrope", - font_size=14, - font_weight=dv.DivFontWeight.REGULAR, - text_color="#808080", - line_height=20, - letter_spacing=0, - ), - ], - actions=[ - # Main selection action - dv.DivAction( - log_id=f"send_contact_{contact.phone.replace('+', '').replace(' ', '')}", - url="divkit://send_contact", - payload={ - "first_name": contact.first_name, - "last_name": contact.last_name, - "phone": contact.phone, - }, - ), - # Success feedback action - dv.DivAction( - log_id=f"send_contact_{idx}_success", - url="div-action://set_variable?name=contact_selection_success&value=1", - ), - ], - ) - items.append(contact_cell) - - # Success feedback container - success_container = dv.DivContainer( - id="contact-selection-success", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{contact_selection_success == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12, bottom=4), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#ECFDF5")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#A7F3D0", width=1) - ), - items=[ - dv.DivText( - text="✅", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["contact_selected"], - font_size=13, - text_color="#065F46", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#065F46", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-contact-selection-success", - url="div-action://set_variable?name=contact_selection_success&value=0", - ) - ], - ), - ], - ) - - # Error feedback container - error_container = dv.DivContainer( - id="contact-selection-error", - orientation=dv.DivContainerOrientation.HORIZONTAL, - visibility=Expr("@{contact_selection_error == 1 ? 'visible' : 'gone'}"), - alignment_horizontal=dv.DivAlignmentHorizontal.CENTER, - width=dv.DivMatchParentSize(), - margins=dv.DivEdgeInsets(top=12, bottom=4), - paddings=dv.DivEdgeInsets(top=10, bottom=10, left=12, right=12), - background=[dv.DivSolidBackground(color="#FEF2F2")], - border=dv.DivBorder( - corner_radius=10, stroke=dv.DivStroke(color="#FECACA", width=1) - ), - items=[ - dv.DivText( - text="⚠️", - font_size=14, - margins=dv.DivEdgeInsets(right=8), - ), - dv.DivText( - text=feedback_texts["contact_error"], - font_size=13, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.MEDIUM, - width=dv.DivMatchParentSize(weight=1), - ), - dv.DivText( - text="✕", - font_size=16, - text_color="#B91C1C", - font_weight=dv.DivFontWeight.BOLD, - paddings=dv.DivEdgeInsets(left=8), - actions=[ - dv.DivAction( - log_id="dismiss-contact-selection-error", - url="div-action://set_variable?name=contact_selection_error&value=0", - ) - ], - ), - ], - ) - - items.append(success_container) - items.append(error_container) - - container = dv.DivContainer( - orientation=dv.DivContainerOrientation.VERTICAL, - items=items, - variables=[ - dv.IntegerVariable(name="contact_selection_success", value=0), - dv.IntegerVariable(name="contact_selection_error", value=0), - ], - margins=dv.DivEdgeInsets( - top=WidgetMargins.TOP.value, - left=WidgetMargins.LEFT.value, - right=WidgetMargins.RIGHT.value, - bottom=WidgetMargins.BOTTOM.value, - ), - paddings=dv.DivEdgeInsets(left=16, right=16, top=8, bottom=8), - ) - container = dv.make_div(container) - with open("logs/json/build_contacts.json", "w") as f: - json.dump(container, f, indent=4) - return container - - -def build_contacts_list(context: Context) -> BuildOutput: - # Extract values from context - llm_output = context.llm_output - backend_output = context.backend_output - version = context.version - language = context.language - chat_id = context.logger_context.chat_id - api_key = context.api_key - logger = context.logger_context.logger - - contacts_list: list[Contact] = [] - - contact_widget = Widget( - order=1, - name="contacts_list", - type="contacts_list", - layout="horizontal", - fields=["contacts"], - values=[{"contacts": contacts_list}], - ) - # text_widget = TextWidget( - # order=1, - # values=[{"text": llm_output}], - # ) - buttons = ButtonsWidget( - order=2, - values=[{"text": "cancel"}], - ) - - for contact in backend_output: - contacts_list.append( - Contact( - first_name=contact["first_name"], - last_name=contact["last_name"], - phone=contact["phone"], - ) - ) - - widgets = add_ui_to_widget( - { - build_contacts_list_widget: WidgetInput( - widget=contact_widget, - args={ - "contacts": contacts_list, - }, - ), - build_buttons_row: WidgetInput( - widget=buttons, - args={"button_texts": ["cancel"], "language": language}, - ), - # build_text_widget: WidgetInput( - # widget=text_widget, - # args={"text": llm_output}, - # ), - }, - version, - ) - - output = BuildOutput( - widgets_count=1, - widgets=[widget.model_dump(exclude_none=True) for widget in widgets], - ) - save_builder_output(context, output) - return output diff --git a/pyrightconfig.json b/pyrightconfig.json deleted file mode 100644 index 6d968ff..0000000 --- a/pyrightconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "exclude": [ - "build", - "dist", - ".venv", - "tests", - "test_requests", - "temp", - ], - "reportMissingImports": true, - "reportUnusedImport": "warning", - "typeCheckingMode": "basic", - "ignore": [ - "reportImplicitOverride", - "reportRedeclaration" - ], - "reportImplicitOverride": false -} \ No newline at end of file diff --git a/src/server.py b/src/server.py index 308902a..ca3152f 100644 --- a/src/server.py +++ b/src/server.py @@ -5,7 +5,10 @@ from conf import logger import os import time +import asyncio +from datetime import datetime from typing import Optional, Any, Dict, List, Callable, Set, Union +from urllib.parse import quote from fastapi import FastAPI, Request, Response from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles @@ -303,3 +306,101 @@ async def format_data_v2(input_data: InputV2): ) return result + + +# --------------------------------------------------------------------------- +# Agent progress: dynamic message delivery +# --------------------------------------------------------------------------- + +AGENT_STEP_MESSAGES: List[str] = [ + "Initializing agent context...", + "Parsing user request...", + "Searching knowledge base...", + "Analyzing retrieved documents...", + "Cross-referencing sources...", + "Generating response draft...", + "Validating response accuracy...", + "Finalizing and formatting output...", +] + +# In-memory state for the running agent +_agent_state: Dict[str, Any] = { + "messages": [], # list of {"step": int, "text": str} + "is_running": False, + "is_complete": False, + "result": None, + "started_at": None, +} + + +async def _deliver_messages_background(messages: List[str]) -> None: + """Background task: add one message every ~2 seconds.""" + global _agent_state + for i, msg in enumerate(messages, start=1): + await asyncio.sleep(2) + if not _agent_state["is_running"]: + return # agent was reset mid-run + _agent_state["messages"].append({"step": i, "text": msg}) + await asyncio.sleep(1) + _agent_state["is_complete"] = True + _agent_state["is_running"] = False + _agent_state["result"] = ( + "Agent completed successfully! The answer to your query has been " + "generated based on the analysis of relevant documents." + ) + + +@app.post("/start_agent") +async def start_agent(): + """Kick off a simulated 8-step agent run.""" + global _agent_state + _agent_state = { + "messages": [], + "is_running": True, + "is_complete": False, + "result": None, + "started_at": datetime.now().isoformat(), + } + asyncio.create_task(_deliver_messages_background(AGENT_STEP_MESSAGES)) + return {"status": "started", "total_steps": len(AGENT_STEP_MESSAGES)} + + +@app.get("/new_messages") +async def new_messages(): + """ + Return the current agent messages. + + The response contains: + - messages: list of delivered messages so far + - is_running: whether the agent is still processing + - is_complete: whether all steps finished + - result: final result text (set once complete) + - total_steps: total number of steps (8) + + The DivKit poll timer hits this endpoint every 2 seconds. + The host application (or DivKit action handler) should read the + response and set the corresponding card variables: + step_N_visible = true + step_N_text = + """ + return { + "messages": _agent_state["messages"], + "is_running": _agent_state["is_running"], + "is_complete": _agent_state["is_complete"], + "result": _agent_state["result"], + "total_steps": len(AGENT_STEP_MESSAGES), + } + + +@app.post("/reset_agent") +async def reset_agent(): + """Reset the agent state back to idle.""" + global _agent_state + _agent_state = { + "messages": [], + "is_running": False, + "is_complete": False, + "result": None, + "started_at": None, + } + return {"status": "reset"} diff --git a/tests/fixtures/request_json/request_function_call_activity.json b/tests/fixtures/request_json/request_function_call_activity.json new file mode 100644 index 0000000..3c0c11d --- /dev/null +++ b/tests/fixtures/request_json/request_function_call_activity.json @@ -0,0 +1,13 @@ +{ + "function_name": "function_call_activity_record", + "llm_output": "Checking your balance and recent transactions.", + "backend_output": { + "function_name": "get_balance", + "arguments": { + "card_id": "card_123", + "include_history": true + } + }, + "chat_id": "chat_abc123", + "api_key": "" +} diff --git a/tests/fixtures/request_json/request_function_response_activity.json b/tests/fixtures/request_json/request_function_response_activity.json new file mode 100644 index 0000000..41114d8 --- /dev/null +++ b/tests/fixtures/request_json/request_function_response_activity.json @@ -0,0 +1,14 @@ +{ + "function_name": "function_response_activity_record", + "llm_output": "Here is your current balance.", + "backend_output": { + "function_name": "get_balance", + "response": { + "balance": 1500.50, + "currency": "UZS", + "last_updated": "2025-02-07T12:00:00Z" + } + }, + "chat_id": "chat_abc123", + "api_key": "" +}