Spaces:
Paused
Paused
| import time | |
| import uuid | |
| import gradio as gr | |
| from dotenv import load_dotenv | |
| from agent import Agent, FETCH_WEBPAGE_TOOL, SHELL_TOOL, READ_TOOL, FINAL_MESSAGE_TOOL | |
| import os | |
| from pathlib import Path | |
| load_dotenv() | |
| gr.set_static_paths("static/") | |
| _SYSTEM_PROMPT = """\ | |
| You are OpenMythos, a powerful AI agent specialized in cybersecurity-related tasks. | |
| You have access to tools that you can use to accomplish your goals. | |
| You are a multi-level vulnerability analysis, a visual dependency risk path, a declared threat level then generates an instant, verifiable hotfix patch before threat actors can exploit it. | |
| When finding exploits list it in multi step use tools to search for something specific if needed. | |
| After finding one bulnerability, you will generate a patch for it and provide a detailed explanation of the vulnerability, including its potential impact and how the patch mitigates the risk. | |
| Than THinks again and search for new vulnerabilities and repeat the process until all vulnerabilities are found. | |
| Don't go much looped if you find yourself in a loop just call the `final_message` tool to end the conversation. | |
| === IMPORTANT: How to end the conversation === | |
| You MUST call the `final_message` tool when you have completed your response and want to end. | |
| If you do NOT call `final_message`, you will be stuck in a loop: | |
| - You respond → system waits for final_message → you did not call it | |
| - → system sends your response back to you → you must respond again | |
| - → this repeats until you call `final_message` | |
| To break out of the loop, simply call `final_message` with no arguments. | |
| Only call `final_message` when you are done or already responded or stuck in a loop. | |
| """ | |
| agent = Agent( | |
| base_url=os.getenv("OPENAI_BASE_URL"), | |
| api_key=os.getenv("OPENAI_API_KEY"), | |
| model=os.getenv("OPENAI_MODEL"), | |
| system_prompt=_SYSTEM_PROMPT, | |
| ) | |
| agent.register_tool(FETCH_WEBPAGE_TOOL, SHELL_TOOL, READ_TOOL, FINAL_MESSAGE_TOOL) | |
| agent.register_all_mcp() | |
| agent.set_final_message_tool() | |
| # Load JS from external files | |
| _js_dir = Path(__file__).parent / "static" / "js" | |
| JS_LOAD_STATE = (_js_dir / "storage.load.js").read_text() | |
| JS_SAVE_STATE = (_js_dir / "storage.save.js").read_text() | |
| LANDING_PAGE_SCRIPT = (_js_dir / "landing.js").read_text() | |
| def _conv_choices(state_value): | |
| convs = sorted(state_value["conversations"], key=lambda c: c.get("last_updated", 0), reverse=True) | |
| return gr.update( | |
| choices=[(c["label"], c["key"]) for c in convs], | |
| value=state_value.get("conversation_id") or None, | |
| ) | |
| class GradioEvents: | |
| """Event handlers for the chatbot UI.""" | |
| def stream_response(message, state_value): | |
| """Stream a chat completion into the active conversation.""" | |
| if not message or not message.strip(): | |
| yield gr.skip() | |
| return | |
| if not state_value.get("conversation_id"): | |
| conv_id = str(uuid.uuid4()) | |
| state_value["conversation_id"] = conv_id | |
| state_value["conversations"].append( | |
| {"label": message[:30], "key": conv_id, "last_updated": int(time.time() * 1000)}) | |
| state_value["conversation_contexts"][conv_id] = { | |
| "history": [] | |
| } | |
| else: | |
| conv_id = state_value["conversation_id"] | |
| state_value["conversation_contexts"].setdefault( | |
| conv_id, {"history": []}) | |
| # Update last_updated for existing conversation | |
| for c in state_value["conversations"]: | |
| if c["key"] == conv_id: | |
| c["last_updated"] = int(time.time() * 1000) | |
| break | |
| ctx = state_value["conversation_contexts"][conv_id] | |
| for c in state_value["conversations"]: | |
| if c["key"] == conv_id and not c.get("label"): | |
| c["label"] = message[:30] | |
| break | |
| ctx["history"].append({"role": "user", "content": message}) | |
| yield { msg: gr.update(value=""), chatbot: gr.update(value=ctx["history"]), state: gr.update(value=state_value), conv_choice: _conv_choices(state_value), send_btn: gr.update(visible=False), stop_btn: gr.update(visible=True)} | |
| # Build display as separate titled messages (smolagents style) | |
| display_messages: list[dict] = list(ctx["history"]) | |
| text_msg_idx: int | None = None | |
| thinking_msg_idx: int | None = None | |
| tool_call_idx: int | None = None | |
| spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] | |
| spinner_idx = 0 | |
| try: | |
| for ev in agent.stream(ctx["history"]): | |
| t = ev["type"] | |
| if t == "reasoning": | |
| spinner_idx += 1 | |
| content = f"<span class=\"thinking-indicator\">{spinner_frames[spinner_idx % len(spinner_frames)]} Thinking...</span>" | |
| if thinking_msg_idx is not None: | |
| display_messages[thinking_msg_idx]["content"] = content | |
| else: | |
| display_messages.append({"role": "assistant", "content": content, "metadata": {}}) | |
| thinking_msg_idx = len(display_messages) - 1 | |
| elif t == "text": | |
| # Remove spinner message if showing — it was a separate bubble | |
| if thinking_msg_idx is not None: | |
| display_messages.pop(thinking_msg_idx) | |
| thinking_msg_idx = None | |
| # Re-index since we popped | |
| if text_msg_idx is not None and thinking_msg_idx is not None: | |
| if text_msg_idx > thinking_msg_idx: | |
| text_msg_idx -= 1 | |
| if text_msg_idx is not None: | |
| display_messages[text_msg_idx]["content"] += ev["content"] | |
| else: | |
| display_messages.append({"role": "assistant", "content": ev["content"], "metadata": {}}) | |
| text_msg_idx = len(display_messages) - 1 | |
| elif t == "tool_call": | |
| # Remove spinner if present | |
| if thinking_msg_idx is not None: | |
| display_messages.pop(thinking_msg_idx) | |
| if text_msg_idx is not None and text_msg_idx > thinking_msg_idx: | |
| text_msg_idx -= 1 | |
| thinking_msg_idx = None | |
| # Finalize any in-flight text message (keep if it has real content) | |
| if text_msg_idx is not None: | |
| c = display_messages[text_msg_idx].get("content", "").strip() | |
| if not c: | |
| display_messages.pop(text_msg_idx) | |
| text_msg_idx = None | |
| tool_call_idx = None | |
| tool_name = ev["name"] | |
| display_messages.append({ | |
| "role": "assistant", | |
| "content": f"```\n{tool_name}({ev['arguments']})\n```\n⏳ Running...", | |
| "metadata": {"title": f"🛠️ Used tool {tool_name}"}, | |
| }) | |
| tool_call_idx = len(display_messages) - 1 | |
| elif t == "tool_output": | |
| if tool_call_idx is not None: | |
| snippet = ev["content"][:500] | |
| cc = snippet if len(ev["content"]) <= 500 else snippet + "\n..." | |
| # Grab the tool name from the existing message | |
| tool_name = display_messages[tool_call_idx]["metadata"]["title"].split("Used tool ")[-1] | |
| display_messages[tool_call_idx]["content"] = ( | |
| f"```\n{tool_name}({ev['arguments']})\n```\n\n" | |
| f"**Output:**\n```\n{cc}\n```" | |
| ) | |
| display_messages[tool_call_idx]["metadata"] = { | |
| "title": f"🛠️ {tool_name} — {len(ev['content'])} chars", | |
| } | |
| # Only clear tool_call_idx if NOT partial (final output) | |
| if not ev.get("partial"): | |
| tool_call_idx = None | |
| elif t == "error": | |
| display_messages.append({ | |
| "role": "assistant", | |
| "content": f'<span style="color: var(--color-red-500)">{ev["content"]}</span>', | |
| "metadata": {"title": "💥 Error"}, | |
| }) | |
| yield { | |
| chatbot: gr.update(value=display_messages), | |
| state: gr.update(value=state_value), | |
| send_btn: gr.update(visible=True), | |
| stop_btn: gr.update(visible=False), | |
| } | |
| return | |
| elif t == "done": | |
| break | |
| # Update history in real-time so state always has latest messages | |
| ctx["history"] = display_messages | |
| yield { | |
| chatbot: gr.update(value=display_messages), | |
| state: gr.update(value=state_value), | |
| } | |
| # Final sync (ensures tool output messages are saved) | |
| ctx["history"] = display_messages | |
| yield { | |
| chatbot: gr.update(value=ctx["history"]), | |
| state: gr.update(value=state_value), | |
| send_btn: gr.update(visible=True), | |
| stop_btn: gr.update(visible=False), | |
| } | |
| except Exception as exc: | |
| display_messages.append({ | |
| "role": "assistant", | |
| "content": f'<span style="color: var(--color-red-400)">{exc}</span>', | |
| "metadata": {"title": "💥 Error"}, | |
| }) | |
| ctx["history"] = display_messages | |
| yield { | |
| chatbot: gr.update(value=display_messages), | |
| state: gr.update(value=state_value), | |
| send_btn: gr.update(visible=True), | |
| stop_btn: gr.update(visible=False), | |
| } | |
| def new_chat(state_value): | |
| state_value["conversation_id"] = "" | |
| return ( | |
| gr.update(value=None), | |
| gr.update(value=None), | |
| gr.update(value=state_value), | |
| ) | |
| def select_conversation(choice, state_value): | |
| if not choice: | |
| return gr.skip() | |
| if choice == state_value.get("conversation_id"): | |
| return gr.skip() | |
| state_value["conversation_id"] = choice | |
| ctx = state_value["conversation_contexts"].get(choice, {}) | |
| return ( | |
| gr.update(value=ctx.get("history", [])), | |
| gr.update(value=state_value), | |
| ) | |
| def delete_selected_conversation(choice, state_value): | |
| if not choice: | |
| return gr.skip() | |
| state_value["conversation_contexts"].pop(choice, None) | |
| state_value["conversations"] = [ | |
| c for c in state_value["conversations"] if c["key"] != choice | |
| ] | |
| was_active = state_value.get("conversation_id") == choice | |
| if was_active: | |
| state_value["conversation_id"] = "" | |
| return ( | |
| _conv_choices(state_value), | |
| gr.update(value=None), | |
| gr.update(value=state_value), | |
| ) | |
| return ( | |
| _conv_choices(state_value), | |
| gr.skip(), | |
| gr.update(value=state_value), | |
| ) | |
| def cancel_stream(state_value): | |
| """Mark the current assistant message as cancelled.""" | |
| if not state_value.get("conversation_id"): | |
| return gr.skip() | |
| ctx = state_value["conversation_contexts"][ | |
| state_value["conversation_id"]] | |
| if ctx.get("history") and ctx["history"][-1].get("role") == "assistant": | |
| ctx["history"][-1]["metadata"] = ctx["history"][-1].get( | |
| "metadata", {}) | |
| ctx["history"][-1]["metadata"]["footer"] = "Chat completion paused" | |
| return (gr.update(value=ctx.get("history", [])), gr.update(value=state_value), gr.update(visible=True), gr.update(visible=False)) | |
| def load_from_js(serialised_json, state_value): | |
| """Receive the JSON string that JS_LOAD_STATE returned, merge into state.""" | |
| import json | |
| if not serialised_json: | |
| return gr.skip(), gr.skip() | |
| try: | |
| loaded = json.loads(serialised_json) | |
| except Exception: | |
| return gr.skip(), gr.skip() | |
| state_value["conversations"] = loaded.get("conversations", []) | |
| state_value["conversation_contexts"] = loaded.get("conversation_contexts", {}) | |
| return _conv_choices(state_value), gr.update(value=state_value) | |
| def prepare_save(state_value): | |
| """Serialise state to JSON so JS_SAVE_STATE can write it to localStorage.""" | |
| import json | |
| return json.dumps({ | |
| "conversations": state_value.get("conversations", []), | |
| "conversation_contexts": state_value.get("conversation_contexts", {}), | |
| }) | |
| with gr.Blocks(fill_width=True, title="OpenMythos Demo") as demo: | |
| state = gr.State({ | |
| "conversation_contexts": {}, | |
| "conversations": [], | |
| "conversation_id": "", | |
| }) | |
| js_load_output = gr.Textbox(visible=False, elem_id="js-load-output") | |
| js_save_input = gr.Textbox(visible=False, elem_id="js-save-input") | |
| with gr.Row(elem_id="main-row"): | |
| with gr.Sidebar(open=False): | |
| new_chat_btn = gr.Button( | |
| value="New Conversation", | |
| variant="primary", | |
| ) | |
| conv_choice = gr.Radio( | |
| choices=[], | |
| label=None, | |
| interactive=True, | |
| elem_id="conversations-radio", | |
| ) | |
| delete_btn = gr.Button( | |
| value="Delete Selected", | |
| variant="stop", | |
| visible=False | |
| ) | |
| with gr.Column(elem_id="chat-column"): | |
| # Landing page shown when chat is empty with added hyperlinks | |
| landing_page = gr.HTML( | |
| value=""" | |
| <div id="landing-page"> | |
| <div class="landing-content"> | |
| <div class="landing-logo"> | |
| <img src="/gradio_api/file=static/svg/logo.svg" alt="MythosHarness" width="420" height="70" /> | |
| </div> | |
| </div> | |
| <div class="landing-prompt"> | |
| <p>Made with ❤️ by <a href="http://huggingface.co/KingNish" target="_blank" style="color: var(--primary-500, #ff4b4b); text-decoration: underline;">KingNish</a> and <a href="https://huggingface.co/himanshu17HF" target="_blank" style="color: var(--primary-500, #ff4b4b); text-decoration: underline;">Himanshu</a></p> | |
| </div> | |
| </div> | |
| """, | |
| elem_id="landing-page-container", | |
| ) | |
| chatbot = gr.Chatbot( | |
| elem_id="chatbot", | |
| show_label=False, | |
| buttons=[], | |
| layout="bubble", | |
| autoscroll=True | |
| ) | |
| with gr.Row(elem_id="input-row"): | |
| msg = gr.Textbox( | |
| placeholder="Enter your message here...", | |
| show_label=False, | |
| scale=4, | |
| container=False, | |
| max_lines=10, | |
| ) | |
| send_btn = gr.Button( | |
| "Send", | |
| variant="primary", | |
| scale=0, | |
| min_width=40, | |
| elem_id="send-btn", | |
| ) | |
| stop_btn = gr.Button( | |
| "Stop", | |
| variant="stop", | |
| scale=0, | |
| min_width=40, | |
| visible=False, | |
| elem_id="stop-btn", | |
| ) | |
| new_chat_btn.click( | |
| fn=GradioEvents.new_chat, | |
| inputs=[state], | |
| outputs=[conv_choice, chatbot, state], | |
| ) | |
| conv_choice.change( | |
| fn=GradioEvents.select_conversation, | |
| inputs=[conv_choice, state], | |
| outputs=[chatbot, state], | |
| ) | |
| delete_btn.click( | |
| fn=GradioEvents.delete_selected_conversation, | |
| inputs=[conv_choice, state], | |
| outputs=[conv_choice, chatbot, state], | |
| ) | |
| submit_event = send_btn.click( | |
| fn=GradioEvents.stream_response, | |
| inputs=[msg, state], | |
| outputs=[msg, chatbot, state, conv_choice, send_btn, stop_btn], | |
| ) | |
| msg.submit( | |
| fn=GradioEvents.stream_response, | |
| inputs=[msg, state], | |
| outputs=[msg, chatbot, state, conv_choice, send_btn, stop_btn], | |
| ) | |
| stop_btn.click( | |
| fn=GradioEvents.cancel_stream, | |
| inputs=[state], | |
| outputs=[chatbot, state, send_btn, stop_btn], | |
| cancels=[submit_event], | |
| ) | |
| state.change( | |
| fn=GradioEvents.prepare_save, | |
| inputs=[state], | |
| outputs=[js_save_input], | |
| ).then( | |
| fn=lambda x: x, | |
| inputs=[js_save_input], | |
| outputs=[js_save_input], | |
| js=JS_SAVE_STATE, | |
| ) | |
| demo.load( | |
| fn=lambda: None, | |
| inputs=None, | |
| outputs=None, | |
| js=LANDING_PAGE_SCRIPT, | |
| ).then( | |
| fn=lambda x: x, | |
| inputs=[js_load_output], | |
| outputs=[js_load_output], | |
| js=JS_LOAD_STATE, | |
| ).then( | |
| fn=GradioEvents.load_from_js, | |
| inputs=[js_load_output, state], | |
| outputs=[conv_choice, state], | |
| ) | |
| theme = gr.themes.Base(radius_size="none") | |
| if __name__ == "__main__": | |
| demo.queue(default_concurrency_limit=100, max_size=100).launch(ssr_mode=False, max_threads=100, css_paths="app.css", theme=theme) |