403Webshell
Server IP : 3.147.158.171  /  Your IP : 216.73.216.216
Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5
System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64
User : ec2-user ( 1000)
PHP Version : 8.4.21
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /tsai/repo/api/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /tsai/repo/api/create_widget.py
"""
Widget Creation Script

Creates a widget on a site by calling the local FastAPI endpoint.

A widget is a Drupal `node--widget` whose entire configuration lives in a JSON
`config` object (mirroring the Creator app's widget form / `createDefaultWidgetConfig`).
This script starts from that default config, applies the named CLI options, then
deep-merges any `--config-file` / `--config-json` overrides on top — which is how
the ~25 feature toggles and nested groups are set without a flag per field.

Usage:
    python create_widget.py <site> --title <title> --subtitle <subtitle> \
        --description <desc> --help-text <help> --icon <icon> [options]

Arguments:
    site                            Site slug (e.g., jpt, tsai, sandbox). Optional
                                    (and ignored) when --system-wide is given.

Required card fields:
    --title <text>                  Widget title (used as the Drupal node title)
    --subtitle <text>               Subtitle / category label
    --description <text>            Dashboard/detail description
    --help-text <text>              Help text shown to users
    --icon <name>                   Material Design icon name (e.g. "auto_awesome")

Options:
    --env <env>                     Environment (default: TSAI_ENV or development)
    --id <slug>                     Widget slug (card.id; default: slugify(title))
    --message <text>                Initial user message example
    --message-hint <text>           Input hint (default: "Enter your message")
    --instructions <text>           AI system instructions (default: helpful assistant)
    --enabled / --disabled          Whether the widget is enabled (default: enabled)
    --hidden                        Hide the widget from the UI (default: visible)
    --shared / --no-shared          Allow other sites to import (default: shared)
    --system-wide                   Create a platform-wide widget owned by the `fapi`
                                    Drupal user instead of a site. It shows on every
                                    site's dashboard (Widget.systemWide == true) and the
                                    `site` argument is not needed. Requires
                                    FAPI_DRUPAL_UUID in the API .env.
    --provider <provider>           OpenAI or Claude (default: Claude)
    --model-name <name>             Model name (default: claude-sonnet-4-6)
    --temperature <t>               Temperature (default: 0.7)
    --max-tokens <n>                Max tokens (default: 4096)
    --no-stream                     Disable streaming responses (default: streaming on)

JSON overrides (deep-merged over the config, in this order, after named args):
    --config-file <path>            Path to a JSON file with a partial/full WidgetConfig
    --config-json '<json>'          Inline JSON with a partial/full WidgetConfig

Examples:
    python create_widget.py jpt --title "Unit Converter" --subtitle "Tools" \
        --description "Convert between units" --help-text "Ask me to convert anything" \
        --icon "straighten"
    python create_widget.py jpt --title "Story Helper" --subtitle "Writing" \
        --description "..." --help-text "..." --icon "auto_stories" \
        --config-json '{"features":{"save":true,"autoSend":{"enabled":true,"intro":"Hi"}}}'
    python create_widget.py --system-wide --title "Word Guess" --subtitle "Games" \
        --description "Wordle-style game" --help-text "Guess the word" --icon "grid_view"
"""

import argparse
import asyncio
import json
import os
import re
import sys
from pathlib import Path

from dotenv import load_dotenv
load_dotenv(dotenv_path=Path(__file__).parent / '.env')

import requests


def default_widget_config() -> dict:
    """Return the default WidgetConfig.

    Mirrors `createDefaultWidgetConfig()` in
    chatbot/projects/tsai/src/lib/services/utils-service.ts and the `card` block
    built in widget-form.ts. Removed fields (animation, features.imageCount,
    features.imageSize) are intentionally absent.
    """
    return {
        "endpoint": "unknown",
        "shared": True,
        "messageHint": "Enter your message",
        "message": "",
        "messageType": "Chat Message",
        "instructions": "You are a helpful assistant.",
        "card": {
            "id": "",
            "title": "",
            "subtitle": "",
            "description": "",
            "help": "",
            "icon": "",
            "quickLinks": [],
            "link": {"name": "Action Link Title", "href": "#"},
            "weight": 10,
            "enabled": True,
            "hidden": False,
            "dynamic": True,
        },
        "features": {
            "prompt": True,
            "new": True,
            "save": False,
            "clear": False,
            "share": False,
            "send": True,
            "enhance": False,
            "refresh": False,
            "uploadImages": False,
            "uploadImagesImageSize": False,
            "downloadImages": False,
            "copyToClipboard": True,
            "copyToPrompt": True,
            "saveImagePrompt": False,
            "createImageFromText": False,
            "autoSend": {"enabled": False, "addToViewer": False, "addUserName": False},
            "prompts": {"enabled": False, "imagePrompt": False},
            # LangChain routing + streaming are both required for the multi-provider
            # (Claude + OpenAI) chat path and the runtime model picker. The plain /chat
            # path is OpenAI-only, so chat widgets default to LangChain. See
            # messenger-base.ts sendMessage (streamedResponses gate) / sendStreamedMessage
            # (useLangChain gate) and responses_langchain.py.
            "streamedResponses": True,
            "useLangChain": True,
            "useStructuredOutput": False,
            "modelSelection": True,
            "useAgentTools": False,
        },
        "model": {
            "provider": "Claude",
            "name": "claude-sonnet-4-6",
            "temperature": 0.7,
            "maxTokens": 16384,
            "stream": True,
        },
    }


def deep_merge(base: dict, override: dict) -> dict:
    """Recursively merge `override` into `base` (mutating and returning base).

    Nested dicts are merged key-by-key; scalars and lists replace wholesale.
    """
    for key, value in override.items():
        if isinstance(value, dict) and isinstance(base.get(key), dict):
            deep_merge(base[key], value)
        else:
            base[key] = value
    return base


def load_site_config(site_slug: str) -> str:
    """Return cb_host for the given site slug."""
    from app.services.site_config_service import SiteConfigService
    service = SiteConfigService()
    site = service.get_site_by_slug(site_slug)
    if site:
        return site.cb_host
    all_slugs = sorted(s.slug for s in service.get_all_sites())
    print(f"Error: site '{site_slug}' not found")
    print(f"Available sites: {', '.join(all_slugs)}")
    sys.exit(1)


def create_systemwide_widget(config: dict, shared: bool):
    """Create a platform-wide widget node owned by the `fapi` Drupal user.

    The FastAPI `/cms/widget/` endpoint always assigns ownership to the site
    resolved from the Origin header, so a system-wide widget can't be made that
    way. Instead we create the node--widget directly against Drupal's JSON:API,
    setting the uid relationship to FAPI_DRUPAL_UUID. Such a node is what the
    backend reports as `Widget.systemWide == true` (drupal.py::create_widget).
    """
    from app.services.drupal import DrupalService

    drupal_host = os.getenv("DRUPAL_HOST")
    drupal_api = os.getenv("DRUPAL_API")
    fapi_uuid = os.getenv("FAPI_DRUPAL_UUID")
    if not fapi_uuid:
        print("Error: FAPI_DRUPAL_UUID is not set in the API .env; cannot create a "
              "system-wide widget.")
        sys.exit(1)

    async def _create():
        service = DrupalService()
        session = requests.Session()
        try:
            login_data = await service.login(session)
            logout_token = login_data.get("logout_token")
            csrf_token = await service.get_csrf_token(session)
            headers = {
                "Content-Type": "application/vnd.api+json",
                "X-CSRF-Token": csrf_token,
                "Authorization": f"Bearer {csrf_token}",
            }
            node_data = {
                "data": {
                    "type": "node--widget",
                    "attributes": {
                        "title": config.get("card", {}).get("title", "widget"),
                        "field_shared2": shared,
                        "body": {"value": json.dumps(config), "format": "plain_text"},
                    },
                    "relationships": {
                        "uid": {"data": {"type": "user--user", "id": fapi_uuid}}
                    },
                }
            }
            url = f"{drupal_host}{drupal_api}/node/widget"
            resp = session.post(url, headers=headers, json=node_data)
            await service.logout(session, logout_token)
            return resp
        finally:
            session.close()

    return asyncio.run(_create())


def main():
    parser = argparse.ArgumentParser(description="Create a widget via the FastAPI endpoint")
    parser.add_argument("site", nargs="?", default=None,
                        help="Site slug (e.g., jpt, tsai). Optional/ignored with --system-wide.")

    # Required card fields
    parser.add_argument("--title", required=True, help="Widget title (Drupal node title)")
    parser.add_argument("--subtitle", required=True, help="Subtitle / category label")
    parser.add_argument("--description", required=True, help="Dashboard/detail description")
    parser.add_argument("--help-text", dest="help_text", required=True, help="Help text shown to users")
    parser.add_argument("--icon", required=True, help="Material Design icon name (e.g. auto_awesome)")

    # Optional card / message
    parser.add_argument("--id", default=None, help="Widget slug for card.id (default: slugify(title))")
    parser.add_argument("--message", default=None, help="Initial user message example")
    parser.add_argument("--message-hint", default=None, help="Input hint (default: 'Enter your message')")
    parser.add_argument("--instructions", default=None, help="AI system instructions")

    enabled_group = parser.add_mutually_exclusive_group()
    enabled_group.add_argument("--enabled", dest="enabled", action="store_true", default=True,
                               help="Create the widget as enabled (default)")
    enabled_group.add_argument("--disabled", dest="enabled", action="store_false",
                               help="Create the widget as disabled")
    parser.add_argument("--hidden", action="store_true", help="Hide the widget from the UI")

    shared_group = parser.add_mutually_exclusive_group()
    shared_group.add_argument("--shared", dest="shared", action="store_true", default=True,
                              help="Allow other sites to import this widget (default)")
    shared_group.add_argument("--no-shared", dest="shared", action="store_false",
                              help="Do not share this widget with other sites")

    parser.add_argument("--system-wide", dest="system_wide", action="store_true",
                        help="Create a platform-wide widget owned by the `fapi` Drupal "
                             "user (shows on every site; no `site` arg needed)")

    # Model
    parser.add_argument("--provider", default="Claude", choices=["OpenAI", "Claude"],
                        help="LLM provider (default: Claude)")
    parser.add_argument("--model-name", default="claude-sonnet-4-6",
                        help="Model name (default: claude-sonnet-4-6)")
    parser.add_argument("--temperature", type=float, default=0.7, help="Temperature (default: 0.7)")
    parser.add_argument("--max-tokens", type=int, default=16384, help="Max tokens (default: 16384)")
    parser.add_argument("--no-stream", dest="stream", action="store_false", default=True,
                        help="Disable streaming responses (default: streaming on)")

    parser.add_argument("--env", default=os.getenv("TSAI_ENV", "development"),
                        choices=["development", "production", "scike_ai"],
                        help="Environment (default: TSAI_ENV or development)")

    # JSON overrides
    parser.add_argument("--config-file", default=None,
                        help="Path to a JSON file with a partial/full WidgetConfig to deep-merge")
    parser.add_argument("--config-json", default=None,
                        help="Inline JSON with a partial/full WidgetConfig to deep-merge")

    args = parser.parse_args()

    slug = args.id or re.sub(r'[^a-z0-9]+', '-', args.title.lower()).strip('-')

    os.environ['TSAI_ENV'] = args.env

    # System-wide widgets are owned by `fapi`, not a site, so no Origin/cb_host
    # lookup is needed. A site-owned widget requires a valid site slug.
    cb_host = None
    if not args.system_wide:
        if not args.site:
            print("Error: a site slug is required unless --system-wide is given.")
            sys.exit(1)
        # Look up cb_host (used as Origin header for site auth)
        cb_host = load_site_config(args.site)

    # 1. Start from defaults
    config = default_widget_config()

    # 2. Apply named args
    config["shared"] = args.shared
    if args.message is not None:
        config["message"] = args.message
    if args.message_hint is not None:
        config["messageHint"] = args.message_hint
    if args.instructions is not None:
        config["instructions"] = args.instructions
    config["card"].update({
        "id": slug,
        "title": args.title,
        "subtitle": args.subtitle,
        "description": args.description,
        "help": args.help_text,
        "icon": args.icon,
        "enabled": args.enabled,
        "hidden": args.hidden,
    })
    config["model"].update({
        "provider": args.provider,
        "name": args.model_name,
        "temperature": args.temperature,
        "maxTokens": args.max_tokens,
        "stream": args.stream,
    })

    # 3. Deep-merge --config-file then --config-json over the config
    if args.config_file:
        try:
            with open(args.config_file) as f:
                file_override = json.load(f)
        except (OSError, json.JSONDecodeError) as e:
            print(f"Error reading --config-file '{args.config_file}': {e}")
            sys.exit(1)
        deep_merge(config, file_override)
    if args.config_json:
        try:
            json_override = json.loads(args.config_json)
        except json.JSONDecodeError as e:
            print(f"Error parsing --config-json: {e}")
            sys.exit(1)
        deep_merge(config, json_override)

    # Keep the node title / mirror fields in sync with the (possibly overridden) card
    card = config.get("card", {})

    owner = "fapi (system-wide)" if args.system_wide else f"site '{args.site}'"
    print(f"Creating widget '{args.title}' for {owner}...")
    print(f"  Slug: {slug}")
    print(f"  Subtitle: {args.subtitle}, Icon: {args.icon}")
    print(f"  Provider: {config['model']['provider']}, Model: {config['model']['name']}")
    print(f"  Enabled: {card.get('enabled')}, Hidden: {card.get('hidden')}, Shared: {config.get('shared')}")

    if args.system_wide:
        # Create the node directly against Drupal with `fapi` ownership.
        try:
            response = create_systemwide_widget(config, config.get("shared", args.shared))
        except requests.ConnectionError:
            print("Error: Could not connect to Drupal (check DRUPAL_HOST in the API .env).")
            sys.exit(1)

        if response.status_code == 201:
            print("System-wide widget created successfully!")
            data = response.json()
            node_id = data.get("data", {}).get("id")
            if node_id:
                print(f"Storage ID (Drupal node): {node_id}")
        else:
            print(f"Error {response.status_code}: {response.text}")
            sys.exit(1)
        return

    body = {
        "widget": {
            "config": config,
            "shared": config.get("shared", args.shared),
            "name": card.get("title", args.title),
            "id": card.get("id", slug),
            "category": card.get("subtitle", args.subtitle),
            "enabled": card.get("enabled", args.enabled),
            "hidden": card.get("hidden", args.hidden),
        }
    }

    url = "http://127.0.0.1:8000/api/v1/cms/widget/"
    headers = {
        "Origin": f"https://{cb_host}",
        "Content-Type": "application/json"
    }

    try:
        response = requests.post(url, json=body, headers=headers, timeout=30)
    except requests.ConnectionError:
        print("Error: Could not connect to FastAPI server at http://127.0.0.1:8000")
        print("Make sure the server is running: cd api && uvicorn app.main:app --reload")
        sys.exit(1)

    if response.status_code in (200, 201):
        print("Widget created successfully!")
        data = response.json()
        print(json.dumps(data, indent=2))

        # Drupal JSON:API node UUID (the widget's storageId).
        node_id = data.get("data", {}).get("data", {}).get("id")
        if node_id:
            print(f"Storage ID (Drupal node): {node_id}")
    else:
        print(f"Error {response.status_code}: {response.text}")
        sys.exit(1)


if __name__ == "__main__":
    main()

Youez - 2016 - github.com/yon3zu
LinuXploit