279 lines
12 KiB
Python
279 lines
12 KiB
Python
"""Chat view — renders both the normal chat interface and the Coding Agent mode."""
|
|
|
|
import streamlit as st
|
|
from backend.managers.chat_manager import ChatManager
|
|
from backend.managers.system_prompter import SystemPrompter
|
|
|
|
import asyncio
|
|
|
|
|
|
# ── Agent Mode helpers ────────────────────────────────────────────────────────
|
|
def _run_async(coro):
|
|
"""Hilfsfunktion um async Code in sync Streamlit auszuführen"""
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
return loop.run_until_complete(coro)
|
|
|
|
def _start_agent(task: str):
|
|
"""Create a new CodingAgent, feed it the task, and propose the first action.
|
|
Stores the agent and its state in session_state so Streamlit can reference
|
|
them across reruns without losing progress.
|
|
"""
|
|
from backend.agent.coding_agent import CodingAgent
|
|
agent = CodingAgent()
|
|
agent.start_task(task)
|
|
action = _run_async(agent.propose_next_action())
|
|
st.session_state.coding_agent = agent
|
|
st.session_state.agent_pending_action = action
|
|
st.session_state.agent_status = "waiting_approval"
|
|
st.session_state.agent_log = []
|
|
|
|
|
|
def _approve_action():
|
|
"""Execute the pending action, log it, then immediately propose the next step."""
|
|
agent = st.session_state.coding_agent
|
|
pending = st.session_state.agent_pending_action
|
|
|
|
result = _run_async(agent.approve())
|
|
|
|
# Append a record to the log so the user can review every completed step.
|
|
st.session_state.agent_log.append({
|
|
"thought": pending.get("thought", ""),
|
|
"tool": result["tool"],
|
|
"arguments": result.get("arguments", {}),
|
|
"result": result["result"],
|
|
})
|
|
|
|
if result["is_done"]:
|
|
# Agent called the "done" tool — task is fully complete.
|
|
st.session_state.agent_status = "done"
|
|
st.session_state.agent_pending_action = None
|
|
else:
|
|
next_action = _run_async(agent.propose_next_action())
|
|
st.session_state.agent_pending_action = next_action
|
|
st.session_state.agent_status = "waiting_approval"
|
|
|
|
|
|
def _reject_action(feedback: str):
|
|
"""Reject the pending action with feedback so the agent replans.
|
|
The pending action is discarded; the agent receives the user's feedback and
|
|
proposes a different approach on the next call to propose_next_action().
|
|
"""
|
|
agent = st.session_state.coding_agent
|
|
agent.reject(feedback or "Please try a different approach.")
|
|
next_action = _run_async(agent.propose_next_action())
|
|
st.session_state.agent_pending_action = next_action
|
|
st.session_state.agent_status = "waiting_approval"
|
|
|
|
|
|
def _followup_agent(question: str):
|
|
"""Continue a finished task by injecting a follow-up question and resuming the loop."""
|
|
agent = st.session_state.coding_agent
|
|
agent.follow_up(question)
|
|
action = _run_async(agent.propose_next_action())
|
|
st.session_state.agent_pending_action = action
|
|
st.session_state.agent_status = "waiting_approval"
|
|
|
|
|
|
def _reset_agent():
|
|
"""Clear all agent state and return to the idle (task input) screen."""
|
|
st.session_state.coding_agent = None
|
|
st.session_state.agent_status = "idle"
|
|
st.session_state.agent_log = []
|
|
st.session_state.agent_pending_action = None
|
|
|
|
|
|
# ── Agent Mode UI ─────────────────────────────────────────────────────────────
|
|
|
|
def render_agent_mode():
|
|
"""Render the step-by-step agent UI.
|
|
|
|
Three distinct screens based on agent_status:
|
|
- "idle" → task description input + Start button
|
|
- "waiting_approval" → show proposed action, Approve / Reject / Abort
|
|
- "done" → success message, follow-up input, New Task button
|
|
"""
|
|
# The toggle must always render so Streamlit keeps agent_mode=True in session_state.
|
|
st.toggle("Agent Mode", key="agent_mode")
|
|
|
|
agent_status = st.session_state.get("agent_status", "idle")
|
|
agent_log = st.session_state.get("agent_log", [])
|
|
|
|
# ── Agent Log ────────────────────────────────────────────────────────────
|
|
# Collapsed by default so it doesn't clutter the UI during active tasks.
|
|
if agent_log:
|
|
with st.expander(f"Agent Log — {len(agent_log)} step(s) completed", expanded=False):
|
|
for i, step in enumerate(agent_log):
|
|
with st.chat_message("assistant"):
|
|
st.markdown(f"**Step {i + 1} — `{step['tool']}`**")
|
|
st.caption(f"Thought: {step['thought']}")
|
|
if step.get("arguments"):
|
|
st.json(step["arguments"])
|
|
result_text = step.get("result", "")
|
|
# Colour the result based on whether the tool succeeded or failed.
|
|
if result_text.startswith("ERROR") or result_text.startswith("SYNTAX ERROR"):
|
|
st.error(result_text)
|
|
elif result_text.startswith("OK") or result_text.startswith("DONE"):
|
|
st.success(result_text)
|
|
else:
|
|
st.code(result_text, language=None)
|
|
|
|
# ── Idle: task input ──────────────────────────────────────────────────────
|
|
if agent_status == "idle":
|
|
task = st.text_area(
|
|
"Describe what the agent should do:",
|
|
key="agent_task_input",
|
|
height=120,
|
|
placeholder="e.g. Write a function that sorts a list and saves it to sorted.py",
|
|
)
|
|
if st.button("Start Agent", type="primary", use_container_width=True):
|
|
#loop = asyncio.new_event_loop()
|
|
#asyncio.set_event_loop(loop)
|
|
if task.strip():
|
|
with st.spinner("Agent is thinking..."):
|
|
_start_agent(task.strip())
|
|
st.rerun()
|
|
else:
|
|
st.warning("Please describe a task first.")
|
|
|
|
# ── Waiting: show proposed action + Approve / Reject ─────────────────────
|
|
elif agent_status == "waiting_approval":
|
|
pending = st.session_state.get("agent_pending_action", {})
|
|
|
|
with st.status("Agent proposes the following step:", expanded=True):
|
|
st.markdown(f"**Thought:** {pending.get('thought', '—')}")
|
|
st.markdown(f"**Tool:** `{pending.get('tool', '—')}`")
|
|
|
|
args = pending.get("arguments", {})
|
|
if args:
|
|
# Show file content separately as a code block for readability;
|
|
# other arguments are displayed as JSON.
|
|
if "content" in args:
|
|
display_args = {k: v for k, v in args.items() if k != "content"}
|
|
if display_args:
|
|
st.json(display_args)
|
|
st.code(args["content"], language="python")
|
|
else:
|
|
st.json(args)
|
|
|
|
feedback = st.text_input(
|
|
"Rejection feedback (optional):",
|
|
key="agent_reject_feedback",
|
|
placeholder="e.g. Use a different approach...",
|
|
)
|
|
|
|
col1, col2, col3 = st.columns([3, 2, 2])
|
|
with col1:
|
|
if st.button("Approve", type="primary", use_container_width=True):
|
|
with st.spinner("Executing and planning next step..."):
|
|
_approve_action()
|
|
st.rerun()
|
|
with col2:
|
|
if st.button("Reject", use_container_width=True):
|
|
with st.spinner("Agent is replanning..."):
|
|
_reject_action(feedback)
|
|
st.rerun()
|
|
with col3:
|
|
if st.button("Abort Task", use_container_width=True):
|
|
_reset_agent()
|
|
st.rerun()
|
|
|
|
# ── Done ─────────────────────────────────────────────────────────────────
|
|
elif agent_status == "done":
|
|
last_result = agent_log[-1]["result"] if agent_log else ""
|
|
st.success(f"Task completed! {last_result}")
|
|
|
|
st.divider()
|
|
|
|
followup = st.text_area(
|
|
"Follow-up question or correction:",
|
|
key="agent_followup_input",
|
|
height=80,
|
|
placeholder="e.g. The output is wrong — it should sort descending. Can you fix that?",
|
|
)
|
|
|
|
col1, col2 = st.columns(2)
|
|
with col1:
|
|
if st.button("Ask Follow-up", type="primary", use_container_width=True):
|
|
if followup.strip():
|
|
with st.spinner("Agent is thinking..."):
|
|
_followup_agent(followup.strip())
|
|
st.rerun()
|
|
else:
|
|
st.warning("Please enter a follow-up question first.")
|
|
with col2:
|
|
if st.button("New Task", use_container_width=True):
|
|
_reset_agent()
|
|
st.rerun()
|
|
|
|
|
|
# ── Normal Chat ───────────────────────────────────────────────────────────────
|
|
|
|
def render_normal_chat():
|
|
"""Render the standard multi-turn chat interface.
|
|
|
|
On the first message the system prompt is injected into the history.
|
|
Each subsequent message appends to the same conversation so the AI retains
|
|
full context throughout the session.
|
|
"""
|
|
# Replay the conversation history as chat bubbles (skip system messages).
|
|
for message in st.session_state.chat_history:
|
|
role = message["role"]
|
|
if role == "system":
|
|
continue
|
|
with st.chat_message(role):
|
|
st.markdown(message["content"])
|
|
|
|
# Chat input — Enter to send, no extra button needed
|
|
user_input = st.chat_input("Type your message here...")
|
|
if user_input:
|
|
chat_manager = st.session_state.chat_manager
|
|
|
|
# On the very first user message, prepend the system prompt so the AI
|
|
# knows it is a code assistant embedded in an editor.
|
|
if not chat_manager.get_history():
|
|
system_prompt = SystemPrompter.generate_prompt()
|
|
chat_manager.add_message("system", system_prompt)
|
|
|
|
# Show user message immediately without waiting for response.
|
|
with st.chat_message("user"):
|
|
st.markdown(user_input)
|
|
|
|
# Call the AI and show its response with a spinner while waiting.
|
|
with st.chat_message("assistant"):
|
|
with st.spinner("Thinking..."):
|
|
try:
|
|
ai_response = chat_manager.send_message(user_input)
|
|
except Exception as e:
|
|
ai_response = f"Error: {e}"
|
|
st.markdown(ai_response)
|
|
|
|
st.session_state.chat_history.append({"role": "user", "content": user_input})
|
|
st.session_state.chat_history.append({"role": "assistant", "content": ai_response})
|
|
st.rerun()
|
|
|
|
# Rendered in the normal flow; JS above clones them to fixed positions
|
|
# and hides these originals.
|
|
st.toggle("Agent Mode", key="agent_mode")
|
|
with st.expander("Settings", expanded=False):
|
|
st.toggle("Use debug system prompt", key="use_system_prompt", value=True)
|
|
|
|
|
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
|
|
def render_chat():
|
|
"""Top-level chat view — switches between Agent Mode and normal chat."""
|
|
if st.session_state.get("agent_mode", False):
|
|
st.subheader("Coding Agent")
|
|
render_agent_mode()
|
|
else:
|
|
st.subheader("Chat with AI Assistant")
|
|
render_normal_chat()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
render_chat()
|