Download inference_debug.py from Berlm/ppt_agent: direct link, hf CLI and curl.
- Browser
- Download file 18.3 kB
-
https://huggingface.co/spaces/Berlm/ppt_agent/resolve/main/inference_debug.py
- Command line
-
hf download hf://spaces/Berlm/ppt_agent/inference_debug.py
-
curl -L -o inference_debug.py https://huggingface.co/spaces/Berlm/ppt_agent/resolve/main/inference_debug.py
18.3 kB
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| import os | |
| import textwrap | |
| from typing import Any | |
| from openai import OpenAI | |
| try: | |
| if os.environ.get("DEBUG", "false").lower() == "true": | |
| from ppt_agent.server.debug_logging import debug_enabled, write_debug_event | |
| else: | |
| raise ImportError("Debug logging is not enabled") | |
| except ImportError: # pragma: no cover | |
| def debug_enabled() -> bool: | |
| return False | |
| def write_debug_event( | |
| event_type: str, payload: dict[str, Any] | None = None | |
| ) -> None: | |
| return None | |
| from agent_action_tools import ( | |
| AgentToolInvocation, | |
| build_openai_tools, | |
| parse_tool_invocation, | |
| tool_invocation_to_action, | |
| ) | |
| from ppt_agent.client import PptAgentEnv | |
| from ppt_agent.models import PptAgentAction | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1") | |
| MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen3.5-27B") | |
| ENV_BASE_URL = os.getenv("ENV_BASE_URL", "http://localhost:7860") | |
| TEMPERATURE = 0.0 | |
| TASKS = [("easy", 10), ("medium", 15), ("hard", 20)] | |
| BENCHMARK = "ppt_agent" | |
| OPENAI_TOOLS = build_openai_tools() | |
| _FALLBACK_SAVE_PATH = "outputs/presentation.pptx" | |
| SYSTEM_PROMPT = textwrap.dedent( | |
| """ | |
| You are an elite presentation designer and PowerPoint author. Your job is to choose exactly one next environment tool call that moves the deck toward a polished, professional final presentation. Make sure to follow the output schema and tool-use rules precisely. | |
| You are given: | |
| - the full task prompt | |
| - the source context | |
| - the current slide count | |
| - the remaining step budget | |
| - the last action error/result | |
| - recent action history | |
| - the current theme | |
| - slide constraints | |
| Available tools: | |
| - create_slide | |
| - update_slide | |
| - delete_slide | |
| - set_theme | |
| - save_presentation | |
| Return no prose. Produce exactly one tool call. | |
| Presentation quality bar: | |
| - Make slides look like strong professional consulting, strategy, product, or investor decks. | |
| - Build a clear narrative, not a random collection of slides. | |
| - Prefer strong hierarchy, clean alignment, generous whitespace, and restrained color usage. | |
| - Avoid clutter, overcrowded text, and weak visual balance. | |
| - Use charts or tables when they communicate evidence better than paragraphs. | |
| - Use concise text with meaningful headlines and clear supporting points. | |
| - Keep the design consistent across the whole deck. | |
| Design guidance: | |
| - Prefer a cohesive deck-level theme. Use set_theme early if the current theme is weak or mismatched to the task. | |
| - Keep typography consistent across slides. | |
| - Prefer readable layouts with clear margins and spacing. | |
| - Use word_wrap, space_before_pt, space_after_pt, and line_spacing when helpful for clean text layout. | |
| Content guidance: | |
| - Ground claims in the provided source context. | |
| - Do not invent facts, numbers, or citations not supported by the inputs. | |
| - If the task implies a specific slide structure, satisfy it. | |
| Tool-use rules: | |
| - Use only the tool schema fields exactly as defined. | |
| - Never invent shorthand slide DSL fields such as: | |
| - background -> use the schema field, not legacy DSL keys | |
| - type: title/body -> use type: text | |
| - font -> use style.font_name | |
| - size -> use numeric style.font_size_pt values | |
| - color -> use style.color_hex or other schema color fields | |
| - Use the provided tools directly. Do not emit plans, explanations, or pseudo-actions. | |
| Shape rules: | |
| - Text shapes require explicit geometry in slide inches: x, y, w, h. | |
| - For new text shapes use type: text. | |
| - For accent bars use type: accent_bar. | |
| - For charts use type: chart with ct, cd, x, y, w, h. | |
| - For tables use type: table with td, x, y, w, h. | |
| - For images use type: image with img, x, y, and optional w, h. | |
| Update rules: | |
| - Use create_slide to add a new slide. | |
| - Use update_slide to revise an existing slide. | |
| - update_slide requires si. | |
| - Existing shapes must be updated by id. | |
| - Use known_named_shapes_by_slide and prior tool results to find the correct id. | |
| - Only use delete_slide when a slide is clearly wrong, redundant, or should be removed. | |
| - Do not iterate too much by updating and deleting slides. Aim to get each new slide right on the first try. | |
| - Use save_presentation when the deck is complete enough and ready to submit. | |
| Theme token rules: | |
| - You may use these theme tokens inside payload values: | |
| - <bg>, <surface>, <accent>, <primary>, <secondary>, <font> | |
| - Prefer theme tokens over hardcoded repeated color and font values for consistent design. | |
| - Use set_theme to update or add more theme tokens in the theme | |
| - Omitted theme keys remain unchanged. | |
| Recommended workflow: | |
| - If needed, set or refine the theme first. | |
| - Think about the slide structure and then create it in one go. | |
| - Call update_slide only if you think it is needed | |
| - You have a finite max step budget. Do not let remaining_steps reach 0 before calling save_presentation. | |
| - Save only when the deck is coherent, complete, and professionally presented. | |
| Example create_slide payload: | |
| { | |
| "bg": "<bg>", | |
| "shapes": [ | |
| { | |
| "type": "text", | |
| "name": "title", | |
| "text": "Harbor Retail Expansion 2026", | |
| "x": 0.7, | |
| "y": 0.6, | |
| "w": 8.6, | |
| "h": 0.8, | |
| "style": { | |
| "font_name": "<font>", | |
| "font_size_pt": 28, | |
| "color_hex": "<primary>", | |
| "bold": true, | |
| "word_wrap": true, | |
| "space_after_pt": 4 | |
| } | |
| }, | |
| { | |
| "type": "text", | |
| "name": "subtitle", | |
| "text": "Growth priorities, risks, and operating implications", | |
| "x": 0.7, | |
| "y": 1.45, | |
| "w": 7.8, | |
| "h": 0.9, | |
| "style": { | |
| "font_name": "<font>", | |
| "font_size_pt": 16, | |
| "color_hex": "<secondary>", | |
| "word_wrap": true, | |
| "line_spacing": 1.15 | |
| } | |
| } | |
| ] | |
| } | |
| Example update_slide payload: | |
| { | |
| "si": 1, | |
| "upd": [ | |
| { | |
| "type": "text", | |
| "id": 11, | |
| "text": "Harbor Retail Expansion 2026", | |
| "style": { | |
| "font_name": "<font>", | |
| "font_size_pt": 28, | |
| "color_hex": "<primary>", | |
| "bold": true | |
| } | |
| } | |
| ], | |
| "add": [ | |
| { | |
| "type": "accent_bar", | |
| "name": "accent", | |
| "hex": "<accent>" | |
| } | |
| ] | |
| } | |
| Example set_theme payload: | |
| { | |
| "bg": "#F8FAFC", | |
| "surface": "#FFFFFF", | |
| "accent": "#2563EB", | |
| "primary": "#0F172A", | |
| "secondary": "#475569", | |
| "font": "Aptos", | |
| "ts": 28, | |
| "bs": 16, | |
| "cs": 10 | |
| } | |
| """ | |
| ).strip() | |
| def log_start(task: str, env: str, model: str) -> None: | |
| print(f"[START] task={task} env={env} model={model}", flush=True) | |
| def log_step( | |
| step: int, action: str, reward: float, done: bool, error: str | None | |
| ) -> None: | |
| error_value = (error or "null").replace("\n", " ") | |
| print( | |
| f"[STEP] step={step} action={action} reward={reward:.2f} done={str(done).lower()} error={error_value}", | |
| flush=True, | |
| ) | |
| def log_end(success: bool, steps: int, score: float, rewards: list[float]) -> None: | |
| rewards_str = ",".join(f"{reward:.2f}" for reward in rewards) | |
| print( | |
| f"[END] success={str(success).lower()} steps={steps} score={score:.2f} rewards={rewards_str}", | |
| flush=True, | |
| ) | |
| def _observation_metadata(observation: Any) -> dict[str, Any]: | |
| metadata = getattr(observation, "metadata", None) | |
| return metadata if isinstance(metadata, dict) else {} | |
| def _planning_payload( | |
| observation: Any, history: list[dict[str, Any]], max_steps: int | |
| ) -> dict[str, Any]: | |
| metadata = _observation_metadata(observation) | |
| current_theme = metadata.get("current_theme") | |
| known_named_shapes = metadata.get("known_named_shapes_by_slide") | |
| slide_constraints = metadata.get("slide_constraints") | |
| default_save_path = metadata.get("default_save_path") | |
| metadata_max_steps = metadata.get("max_steps") | |
| effective_max_steps = ( | |
| min(metadata_max_steps, max_steps) | |
| if isinstance(metadata_max_steps, int) | |
| else max_steps | |
| ) | |
| return { | |
| "task_prompt": observation.task_prompt, | |
| "source_context": observation.source_context, | |
| "slide_count": observation.slide_count, | |
| "remaining_steps": max( | |
| 0, | |
| effective_max_steps - int(metadata.get("step_count", 0)), | |
| ), | |
| "last_action_error": observation.last_action_error, | |
| "last_action_result": observation.last_action_result, | |
| "recent_actions": history[-5:], | |
| "known_named_shapes_by_slide": ( | |
| known_named_shapes if isinstance(known_named_shapes, dict) else {} | |
| ), | |
| "current_theme": current_theme if isinstance(current_theme, dict) else {}, | |
| "slide_constraints": ( | |
| slide_constraints if isinstance(slide_constraints, dict) else {} | |
| ), | |
| "default_save_path": ( | |
| default_save_path | |
| if isinstance(default_save_path, str) and default_save_path | |
| else _FALLBACK_SAVE_PATH | |
| ), | |
| } | |
| def _validate_tool_choice(invocation: AgentToolInvocation, observation: Any) -> None: | |
| if invocation.tool_name == "save_presentation": | |
| constraints = _observation_metadata(observation).get("slide_constraints") | |
| min_slides = ( | |
| constraints.get("min_slides") if isinstance(constraints, dict) else None | |
| ) | |
| if isinstance(min_slides, int) and observation.slide_count < min_slides: | |
| raise ValueError( | |
| "cannot save before reaching the required minimum slide count" | |
| ) | |
| return | |
| if invocation.tool_name in {"update_slide", "delete_slide"}: | |
| slide_index = invocation.arguments.get("slide_index") | |
| if not isinstance(slide_index, int): | |
| raise ValueError(f"{invocation.tool_name} requires slide_index") | |
| if slide_index > observation.slide_count: | |
| raise ValueError( | |
| f"slide_index {slide_index} is out of range for {observation.slide_count} slides" | |
| ) | |
| def _extract_tool_invocation( | |
| message: Any, | |
| ) -> tuple[AgentToolInvocation, dict[str, Any]]: | |
| raw_tool_calls = getattr(message, "tool_calls", None) or [] | |
| tool_calls = list(raw_tool_calls) | |
| if not tool_calls: | |
| raise ValueError("LLM must emit at least one tool call") | |
| tool_call = tool_calls[0] | |
| function = getattr(tool_call, "function", None) | |
| tool_name = getattr(function, "name", None) | |
| arguments = getattr(function, "arguments", None) or "{}" | |
| if not isinstance(tool_name, str) or not tool_name: | |
| raise ValueError("Tool call missing function name") | |
| return ( | |
| parse_tool_invocation(tool_name, arguments), | |
| { | |
| "tool_name": tool_name, | |
| "arguments": arguments, | |
| "ignored_tool_calls": max(0, len(tool_calls) - 1), | |
| }, | |
| ) | |
| def _history_entry_from_tool_call( | |
| invocation: AgentToolInvocation, observation: Any | |
| ) -> dict[str, Any]: | |
| last_action_result = ( | |
| observation.last_action_result | |
| if isinstance(observation.last_action_result, dict) | |
| else {} | |
| ) | |
| slide_index = last_action_result.get("slide_index") | |
| raw_tool_result = last_action_result.get("tool_result") | |
| tool_result = raw_tool_result if isinstance(raw_tool_result, dict) else None | |
| return { | |
| "tool_name": invocation.tool_name, | |
| "arguments": invocation.arguments, | |
| "slide_index": slide_index if isinstance(slide_index, int) else None, | |
| "tool_result": tool_result, | |
| "error": observation.last_action_error, | |
| } | |
| def _message_debug_payload(message: Any) -> dict[str, Any]: | |
| tool_calls = getattr(message, "tool_calls", None) or [] | |
| return { | |
| "content": getattr(message, "content", None), | |
| "tool_calls": [ | |
| { | |
| "id": getattr(tool_call, "id", None), | |
| "type": getattr(tool_call, "type", None), | |
| "function": { | |
| "name": getattr(getattr(tool_call, "function", None), "name", None), | |
| "arguments": getattr( | |
| getattr(tool_call, "function", None), "arguments", None | |
| ), | |
| }, | |
| } | |
| for tool_call in tool_calls | |
| ], | |
| } | |
| def choose_action( | |
| client: OpenAI, observation: Any, history: list[dict[str, Any]], max_steps: int | |
| ) -> tuple[PptAgentAction, str, AgentToolInvocation]: | |
| metadata = _observation_metadata(observation) | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| { | |
| "role": "user", | |
| "content": json.dumps( | |
| _planning_payload(observation, history, max_steps), | |
| separators=(",", ":"), | |
| ), | |
| }, | |
| ] | |
| raw_response: dict[str, Any] | None = None | |
| request_payload = { | |
| "stage": "chat.tools", | |
| "model": MODEL_NAME, | |
| "base_url": API_BASE_URL, | |
| "temperature": TEMPERATURE, | |
| "max_tokens": 8192, | |
| "tool_choice": "required", | |
| "messages": messages, | |
| "tools": OPENAI_TOOLS, | |
| } | |
| if debug_enabled(): | |
| write_debug_event("llm.request", request_payload) | |
| try: | |
| completion = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=messages, | |
| temperature=TEMPERATURE, | |
| max_tokens=8192, | |
| tools=OPENAI_TOOLS, | |
| tool_choice="required", | |
| ) | |
| message = completion.choices[0].message | |
| if debug_enabled(): | |
| write_debug_event( | |
| "llm.response", | |
| { | |
| "stage": "chat.tools", | |
| "model": MODEL_NAME, | |
| "message": _message_debug_payload(message), | |
| }, | |
| ) | |
| invocation, raw_response = _extract_tool_invocation(message) | |
| _validate_tool_choice(invocation, observation) | |
| action = tool_invocation_to_action( | |
| invocation, | |
| default_save_path=( | |
| metadata.get("default_save_path") | |
| if isinstance(metadata.get("default_save_path"), str) | |
| else _FALLBACK_SAVE_PATH | |
| ), | |
| ) | |
| return ( | |
| action, | |
| json.dumps( | |
| { | |
| "action_type": action.action_type, | |
| "slide_index": action.slide_index, | |
| "payload": action.payload, | |
| }, | |
| separators=(",", ":"), | |
| ), | |
| invocation, | |
| ) | |
| except Exception as error: | |
| if debug_enabled(): | |
| write_debug_event( | |
| "llm.error", | |
| { | |
| "stage": "chat.tools", | |
| "model": MODEL_NAME, | |
| "base_url": API_BASE_URL, | |
| "error_type": type(error).__name__, | |
| "error": str(error), | |
| "response": raw_response, | |
| }, | |
| ) | |
| print(f"[DEBUG] choose_action failed: {error}", flush=True) | |
| if raw_response is not None: | |
| print( | |
| "[DEBUG] choose_action response=" | |
| + json.dumps(raw_response, separators=(",", ":")), | |
| flush=True, | |
| ) | |
| raise | |
| async def run_task(difficulty: str, max_steps: int) -> None: | |
| client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN) | |
| rewards: list[float] = [] | |
| history: list[dict[str, Any]] = [] | |
| steps_taken = 0 | |
| score = 0.0 | |
| success = False | |
| started = False | |
| try: | |
| async with PptAgentEnv(base_url=ENV_BASE_URL, message_timeout_s=180.0) as env: | |
| result = await env.reset(difficulty=difficulty) | |
| observation = result.observation | |
| log_start( | |
| task=observation.task_name or "unknown", | |
| env=BENCHMARK, | |
| model=MODEL_NAME, | |
| ) | |
| started = True | |
| for step in range(1, max_steps + 1): | |
| if result.done: | |
| break | |
| # choose_action performs a synchronous model request. Keep it off | |
| # the event loop so websocket heartbeat traffic can still flow. | |
| action, action_str, invocation = await asyncio.to_thread( | |
| choose_action, | |
| client, | |
| observation, | |
| history, | |
| max_steps, | |
| ) | |
| result = await env.step(action) | |
| observation = result.observation | |
| reward = float(result.reward or 0.0) | |
| rewards.append(reward) | |
| history.append(_history_entry_from_tool_call(invocation, observation)) | |
| steps_taken = step | |
| log_step( | |
| step=step, | |
| action=action_str, | |
| reward=reward, | |
| done=result.done, | |
| error=observation.last_action_error, | |
| ) | |
| if result.done: | |
| break | |
| else: | |
| print( | |
| f"[DEBUG] reached max steps {max_steps} without completion", | |
| flush=True, | |
| ) | |
| success = bool( | |
| started | |
| and result.done | |
| and not observation.last_action_error | |
| and observation.score > 0.0 | |
| ) | |
| score = float(observation.score or 0.0) | |
| except Exception as error: | |
| print(f"[DEBUG] inference failed: {error}", flush=True) | |
| success = False | |
| finally: | |
| log_end(success=success, steps=steps_taken, score=score, rewards=rewards) | |
| async def main() -> None: | |
| for difficulty, max_steps in TASKS: | |
| await run_task(difficulty, max_steps) | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |