Spaces:
Runtime error
Runtime error
Upload 5 files
Browse filesImplement persistent chat sessions and Word document output: add session sidebar and chat management via DB, generate docx on finalize; update search caching and UI.
- app.py +234 -27
- db.py +53 -1
- docx_utils.py +90 -0
- requirements.txt +2 -0
app.py
CHANGED
|
@@ -7,7 +7,24 @@ from generators import generate_course_zip
|
|
| 7 |
from searcher import web_search, fetch_and_extract, get_youtube_transcript
|
| 8 |
|
| 9 |
# Bring in DB helpers to persist resources if needed later
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
# System prompt guiding the assistant's behaviour during brainstorming
|
| 13 |
SYSTEM_PROMPT = (
|
|
@@ -17,8 +34,27 @@ SYSTEM_PROMPT = (
|
|
| 17 |
"conversation and gathered resources."
|
| 18 |
)
|
| 19 |
|
| 20 |
-
def chat(user_message, chat_history, chat_pairs, sources, plan, resource_cache):
|
| 21 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
# Ensure lists/dicts are initialised
|
| 23 |
if chat_history is None:
|
| 24 |
chat_history = []
|
|
@@ -26,6 +62,13 @@ def chat(user_message, chat_history, chat_pairs, sources, plan, resource_cache):
|
|
| 26 |
chat_pairs = []
|
| 27 |
if resource_cache is None:
|
| 28 |
resource_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
# Append the user's message to the conversation history (list of dictionaries for Chatbot)
|
| 30 |
chat_history.append({"role": "user", "content": user_message})
|
| 31 |
# Build messages including system prompt for API call
|
|
@@ -289,6 +332,12 @@ def chat(user_message, chat_history, chat_pairs, sources, plan, resource_cache):
|
|
| 289 |
)
|
| 290 |
# Append assistant reply to conversation history
|
| 291 |
chat_history.append({"role": "assistant", "content": assistant_reply})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
# Append pair to display history for any other uses (kept for compatibility)
|
| 293 |
chat_pairs.append((user_message, assistant_reply))
|
| 294 |
# For Chatbot with default type (list of (user, assistant) tuples), return chat_pairs as the first output
|
|
@@ -328,7 +377,7 @@ def run_search(query, chat_history, chat_pairs, sources, plan, num_results=5, do
|
|
| 328 |
return summary, chat_history, chat_pairs, sources, plan
|
| 329 |
|
| 330 |
def finalize_outline(chat_history, chat_pairs, sources, plan):
|
| 331 |
-
"""Generate a course outline based on the conversation and collected sources."""
|
| 332 |
if chat_history is None:
|
| 333 |
chat_history = []
|
| 334 |
if sources is None:
|
|
@@ -344,6 +393,54 @@ def finalize_outline(chat_history, chat_pairs, sources, plan):
|
|
| 344 |
plan = plan_text
|
| 345 |
return plan_text, chat_history, chat_pairs, sources, plan
|
| 346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
def generate_package(plan, sources):
|
| 348 |
"""Generate the final course package zip file."""
|
| 349 |
if plan is None or plan == "":
|
|
@@ -369,41 +466,151 @@ def generate_package(plan, sources):
|
|
| 369 |
with gr.Blocks() as demo:
|
| 370 |
gr.Markdown(
|
| 371 |
"""# Course Creator Agent
|
| 372 |
-
Chat with the assistant to brainstorm your course idea. You can ask the assistant to search the internet directly in the chat. When you're ready, click **Finalize Outline** to generate a course plan. Then generate the final course package (ZIP)."""
|
| 373 |
)
|
| 374 |
-
#
|
| 375 |
-
|
| 376 |
-
# messages type and makes the conversation visible without custom styling. Specify a height to
|
| 377 |
-
# constrain the display area.
|
| 378 |
-
chatbot = gr.Chatbot(label="Conversation", height=400)
|
| 379 |
-
msg_input = gr.Textbox(label="Your message", placeholder="Type your message and press Enter", lines=1)
|
| 380 |
-
# Buttons and outputs (search is triggered via chat; no separate search controls)
|
| 381 |
-
finalize_btn = gr.Button("Finalize Outline")
|
| 382 |
-
plan_output = gr.Textbox(label="Course outline", interactive=False)
|
| 383 |
-
generate_btn = gr.Button("Generate Course Package")
|
| 384 |
-
file_output = gr.File(label="course.zip")
|
| 385 |
-
# State variables to keep track of conversation (messages), display pairs, sources and plan
|
| 386 |
state_chat_history = gr.State([])
|
| 387 |
state_chat_pairs = gr.State([])
|
| 388 |
state_sources = gr.State([])
|
| 389 |
state_plan = gr.State("")
|
| 390 |
-
# Cache for previously fetched search results (keyed by lowercased query). Helps prevent
|
| 391 |
-
# duplicate web searches and ensures all returned resources are real and consistent.
|
| 392 |
state_resource_cache = gr.State({})
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
msg_input.submit(
|
| 395 |
chat,
|
| 396 |
-
inputs=[msg_input, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
|
| 397 |
-
# First output goes to the Chatbot; return the updated chat pairs for proper display
|
| 398 |
outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
|
| 399 |
)
|
| 400 |
-
#
|
| 401 |
finalize_btn.click(
|
| 402 |
-
|
| 403 |
-
inputs=[state_chat_history, state_chat_pairs, state_sources, state_plan],
|
| 404 |
-
outputs=[plan_output, state_chat_history, state_chat_pairs, state_sources, state_plan],
|
| 405 |
)
|
| 406 |
-
# Generate course package
|
| 407 |
generate_btn.click(
|
| 408 |
generate_package,
|
| 409 |
inputs=[state_plan, state_sources],
|
|
|
|
| 7 |
from searcher import web_search, fetch_and_extract, get_youtube_transcript
|
| 8 |
|
| 9 |
# Bring in DB helpers to persist resources if needed later
|
| 10 |
+
# Import DB helpers. We include list_chats, rename_chat and delete_chat to support
|
| 11 |
+
# persistent chat sessions and management actions. The resource helpers allow
|
| 12 |
+
# fetching cached resources if needed.
|
| 13 |
+
from db import (
|
| 14 |
+
get_resource,
|
| 15 |
+
upsert_resource,
|
| 16 |
+
list_resources,
|
| 17 |
+
new_chat,
|
| 18 |
+
append_message,
|
| 19 |
+
load_chat,
|
| 20 |
+
soft_delete_message,
|
| 21 |
+
list_chats,
|
| 22 |
+
rename_chat,
|
| 23 |
+
delete_chat,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Import the docx utility to generate Word documents for course outlines.
|
| 27 |
+
from docx_utils import outline_to_docx
|
| 28 |
|
| 29 |
# System prompt guiding the assistant's behaviour during brainstorming
|
| 30 |
SYSTEM_PROMPT = (
|
|
|
|
| 34 |
"conversation and gathered resources."
|
| 35 |
)
|
| 36 |
|
| 37 |
+
def chat(user_message, chat_history, chat_pairs, sources, plan, resource_cache, chat_key):
|
| 38 |
+
"""
|
| 39 |
+
Handle a user chat message and return updated chat state.
|
| 40 |
+
|
| 41 |
+
This version persists messages to the database by inserting user and assistant
|
| 42 |
+
messages into the `messages` table keyed by `chat_key`. It also returns
|
| 43 |
+
updated state variables for Gradio to reflect the conversation.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
user_message: The latest user input from the textbox.
|
| 47 |
+
chat_history: List of message dicts representing the full conversation.
|
| 48 |
+
chat_pairs: List of (user, assistant) tuples for display in the Chatbot.
|
| 49 |
+
sources: List of collected resource dicts with 'title' and 'url'.
|
| 50 |
+
plan: The current course plan text (unused here).
|
| 51 |
+
resource_cache: Dictionary caching search results by query.
|
| 52 |
+
chat_key: The unique key identifying the current chat session.
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
Tuple of updated (chat_pairs, chat_history, chat_pairs, sources, plan,
|
| 56 |
+
resource_cache).
|
| 57 |
+
"""
|
| 58 |
# Ensure lists/dicts are initialised
|
| 59 |
if chat_history is None:
|
| 60 |
chat_history = []
|
|
|
|
| 62 |
chat_pairs = []
|
| 63 |
if resource_cache is None:
|
| 64 |
resource_cache = {}
|
| 65 |
+
# Persist the user message to the database if a chat key is provided
|
| 66 |
+
if chat_key:
|
| 67 |
+
try:
|
| 68 |
+
append_message(chat_key, "user", user_message)
|
| 69 |
+
except Exception:
|
| 70 |
+
# Ignore DB errors; continue without persistence
|
| 71 |
+
pass
|
| 72 |
# Append the user's message to the conversation history (list of dictionaries for Chatbot)
|
| 73 |
chat_history.append({"role": "user", "content": user_message})
|
| 74 |
# Build messages including system prompt for API call
|
|
|
|
| 332 |
)
|
| 333 |
# Append assistant reply to conversation history
|
| 334 |
chat_history.append({"role": "assistant", "content": assistant_reply})
|
| 335 |
+
# Persist assistant message to the database
|
| 336 |
+
if chat_key:
|
| 337 |
+
try:
|
| 338 |
+
append_message(chat_key, "assistant", assistant_reply)
|
| 339 |
+
except Exception:
|
| 340 |
+
pass
|
| 341 |
# Append pair to display history for any other uses (kept for compatibility)
|
| 342 |
chat_pairs.append((user_message, assistant_reply))
|
| 343 |
# For Chatbot with default type (list of (user, assistant) tuples), return chat_pairs as the first output
|
|
|
|
| 377 |
return summary, chat_history, chat_pairs, sources, plan
|
| 378 |
|
| 379 |
def finalize_outline(chat_history, chat_pairs, sources, plan):
|
| 380 |
+
"""Generate a course outline based on the conversation and collected sources (text only)."""
|
| 381 |
if chat_history is None:
|
| 382 |
chat_history = []
|
| 383 |
if sources is None:
|
|
|
|
| 393 |
plan = plan_text
|
| 394 |
return plan_text, chat_history, chat_pairs, sources, plan
|
| 395 |
|
| 396 |
+
|
| 397 |
+
def finalize_and_doc(chat_history, chat_pairs, sources, plan, chat_key):
|
| 398 |
+
"""
|
| 399 |
+
Generate a course outline and a Word document from the conversation and sources.
|
| 400 |
+
|
| 401 |
+
This function calls the planner to create a textual plan, then writes the plan and
|
| 402 |
+
references to a .docx file using the docx utility. It returns the plan text,
|
| 403 |
+
updated state variables, and the path to the generated document.
|
| 404 |
+
|
| 405 |
+
Args:
|
| 406 |
+
chat_history: Conversation messages list.
|
| 407 |
+
chat_pairs: Display pairs list.
|
| 408 |
+
sources: List of collected resources (dictionaries with title and url).
|
| 409 |
+
plan: The existing plan text (ignored here).
|
| 410 |
+
chat_key: The key identifying the current chat (unused here but kept for consistency).
|
| 411 |
+
|
| 412 |
+
Returns:
|
| 413 |
+
Tuple of (plan_text, chat_history, chat_pairs, sources, plan_text, doc_path).
|
| 414 |
+
"""
|
| 415 |
+
if chat_history is None:
|
| 416 |
+
chat_history = []
|
| 417 |
+
if sources is None:
|
| 418 |
+
sources = []
|
| 419 |
+
# Generate the course plan text
|
| 420 |
+
try:
|
| 421 |
+
plan_text = plan_course(chat_history, sources)
|
| 422 |
+
except Exception as e:
|
| 423 |
+
plan_text = (
|
| 424 |
+
"An error occurred while generating the course outline. Please ensure your API keys are configured.\n"
|
| 425 |
+
f"(Error: {e})"
|
| 426 |
+
)
|
| 427 |
+
# Create a Word document from the plan text and sources
|
| 428 |
+
try:
|
| 429 |
+
doc_path = outline_to_docx("Course Outline", plan_text, references=sources)
|
| 430 |
+
except Exception as e:
|
| 431 |
+
# If DOCX generation fails, create a temporary text file with an error
|
| 432 |
+
err_msg = (
|
| 433 |
+
"An error occurred while creating the Word document.\n"
|
| 434 |
+
f"(Error: {e})"
|
| 435 |
+
)
|
| 436 |
+
tmp_path = "/tmp/outline_error.txt"
|
| 437 |
+
with open(tmp_path, "w") as f:
|
| 438 |
+
f.write(err_msg)
|
| 439 |
+
doc_path = tmp_path
|
| 440 |
+
# Update plan state
|
| 441 |
+
plan = plan_text
|
| 442 |
+
return plan_text, chat_history, chat_pairs, sources, plan, doc_path
|
| 443 |
+
|
| 444 |
def generate_package(plan, sources):
|
| 445 |
"""Generate the final course package zip file."""
|
| 446 |
if plan is None or plan == "":
|
|
|
|
| 466 |
with gr.Blocks() as demo:
|
| 467 |
gr.Markdown(
|
| 468 |
"""# Course Creator Agent
|
| 469 |
+
Chat with the assistant to brainstorm your course idea. Use the panel on the left to manage multiple chat sessions (create, rename, delete). You can ask the assistant to search the internet directly in the chat. When you're ready, click **Finalize Outline** to generate a course plan and a Word document. Then generate the final course package (ZIP)."""
|
| 470 |
)
|
| 471 |
+
# Global states
|
| 472 |
+
state_chat_key = gr.State(new_chat())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
state_chat_history = gr.State([])
|
| 474 |
state_chat_pairs = gr.State([])
|
| 475 |
state_sources = gr.State([])
|
| 476 |
state_plan = gr.State("")
|
|
|
|
|
|
|
| 477 |
state_resource_cache = gr.State({})
|
| 478 |
+
|
| 479 |
+
# Define layout with a sidebar for sessions and a main panel for chat
|
| 480 |
+
with gr.Row():
|
| 481 |
+
with gr.Column(scale=1, min_width=220):
|
| 482 |
+
session_picker = gr.Radio(label="Your Chats", choices=[], interactive=True)
|
| 483 |
+
new_chat_btn = gr.Button("New Chat")
|
| 484 |
+
rename_input = gr.Textbox(label="Rename chat", placeholder="New title", lines=1)
|
| 485 |
+
rename_btn = gr.Button("Rename")
|
| 486 |
+
delete_btn = gr.Button("Delete Chat")
|
| 487 |
+
with gr.Column(scale=4):
|
| 488 |
+
chatbot = gr.Chatbot(label="Conversation", height=400)
|
| 489 |
+
msg_input = gr.Textbox(
|
| 490 |
+
label="Your message",
|
| 491 |
+
placeholder="Type your message and press Enter",
|
| 492 |
+
lines=1,
|
| 493 |
+
)
|
| 494 |
+
finalize_btn = gr.Button("Finalize Outline")
|
| 495 |
+
plan_output = gr.Textbox(label="Course outline", interactive=False)
|
| 496 |
+
doc_output = gr.File(label="Course outline (Word)")
|
| 497 |
+
generate_btn = gr.Button("Generate Course Package")
|
| 498 |
+
file_output = gr.File(label="course.zip")
|
| 499 |
+
|
| 500 |
+
# Helper to refresh the sidebar session list. Returns an update for session_picker.
|
| 501 |
+
def refresh_sessions():
|
| 502 |
+
chats = list_chats()
|
| 503 |
+
choices = [c["key"] for c in chats]
|
| 504 |
+
# Display titles or truncated key if no title
|
| 505 |
+
labels = [c["title"] if c["title"] else c["key"][:8] for c in chats]
|
| 506 |
+
# gr.Radio uses choices for values and may display them as-is; set label implicitly via choices
|
| 507 |
+
return gr.update(choices=choices, value=state_chat_key.value)
|
| 508 |
+
|
| 509 |
+
# Load the selected chat into memory and return display pairs/history
|
| 510 |
+
def load_session(selected_key):
|
| 511 |
+
if not selected_key:
|
| 512 |
+
return [], [], [], [], "", {}
|
| 513 |
+
msgs = load_chat(selected_key)
|
| 514 |
+
history = []
|
| 515 |
+
pairs = []
|
| 516 |
+
buffer = []
|
| 517 |
+
for msg in msgs:
|
| 518 |
+
role = msg["role"]
|
| 519 |
+
content = msg["content"]
|
| 520 |
+
history.append({"role": role, "content": content})
|
| 521 |
+
if role == "user":
|
| 522 |
+
buffer = [content, ""]
|
| 523 |
+
else:
|
| 524 |
+
if buffer:
|
| 525 |
+
buffer[1] = content
|
| 526 |
+
pairs.append(tuple(buffer))
|
| 527 |
+
buffer = []
|
| 528 |
+
return pairs, history, pairs, [], "", {}
|
| 529 |
+
|
| 530 |
+
# Create a new chat session and return the new key
|
| 531 |
+
def handle_new_chat():
|
| 532 |
+
key = new_chat()
|
| 533 |
+
return key
|
| 534 |
+
|
| 535 |
+
# Rename the current chat session
|
| 536 |
+
def handle_rename(chat_key, new_title):
|
| 537 |
+
if chat_key and new_title:
|
| 538 |
+
rename_chat(chat_key, new_title)
|
| 539 |
+
return ""
|
| 540 |
+
|
| 541 |
+
# Delete the current chat session and return a new key to switch to
|
| 542 |
+
def handle_delete(chat_key):
|
| 543 |
+
if chat_key:
|
| 544 |
+
delete_chat(chat_key)
|
| 545 |
+
chats = list_chats()
|
| 546 |
+
if chats:
|
| 547 |
+
return chats[0]["key"]
|
| 548 |
+
else:
|
| 549 |
+
return new_chat()
|
| 550 |
+
|
| 551 |
+
# Initialize session list on load
|
| 552 |
+
demo.load(
|
| 553 |
+
lambda: refresh_sessions(),
|
| 554 |
+
None,
|
| 555 |
+
[session_picker],
|
| 556 |
+
)
|
| 557 |
+
# When a session is selected, load it
|
| 558 |
+
session_picker.change(
|
| 559 |
+
load_session,
|
| 560 |
+
inputs=session_picker,
|
| 561 |
+
outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
|
| 562 |
+
)
|
| 563 |
+
# New chat button
|
| 564 |
+
new_chat_btn.click(
|
| 565 |
+
handle_new_chat,
|
| 566 |
+
inputs=None,
|
| 567 |
+
outputs=state_chat_key,
|
| 568 |
+
).then(
|
| 569 |
+
lambda: refresh_sessions(),
|
| 570 |
+
None,
|
| 571 |
+
[session_picker],
|
| 572 |
+
).then(
|
| 573 |
+
load_session,
|
| 574 |
+
inputs=state_chat_key,
|
| 575 |
+
outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
|
| 576 |
+
)
|
| 577 |
+
# Rename button
|
| 578 |
+
rename_btn.click(
|
| 579 |
+
handle_rename,
|
| 580 |
+
inputs=[state_chat_key, rename_input],
|
| 581 |
+
outputs=rename_input,
|
| 582 |
+
).then(
|
| 583 |
+
lambda: refresh_sessions(),
|
| 584 |
+
None,
|
| 585 |
+
[session_picker],
|
| 586 |
+
)
|
| 587 |
+
# Delete button
|
| 588 |
+
delete_btn.click(
|
| 589 |
+
handle_delete,
|
| 590 |
+
inputs=state_chat_key,
|
| 591 |
+
outputs=state_chat_key,
|
| 592 |
+
).then(
|
| 593 |
+
lambda: refresh_sessions(),
|
| 594 |
+
None,
|
| 595 |
+
[session_picker],
|
| 596 |
+
).then(
|
| 597 |
+
load_session,
|
| 598 |
+
inputs=state_chat_key,
|
| 599 |
+
outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
|
| 600 |
+
)
|
| 601 |
+
# Chat submission: include chat_key for persistence
|
| 602 |
msg_input.submit(
|
| 603 |
chat,
|
| 604 |
+
inputs=[msg_input, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache, state_chat_key],
|
|
|
|
| 605 |
outputs=[chatbot, state_chat_history, state_chat_pairs, state_sources, state_plan, state_resource_cache],
|
| 606 |
)
|
| 607 |
+
# Finalise outline and produce Word doc
|
| 608 |
finalize_btn.click(
|
| 609 |
+
finalize_and_doc,
|
| 610 |
+
inputs=[state_chat_history, state_chat_pairs, state_sources, state_plan, state_chat_key],
|
| 611 |
+
outputs=[plan_output, state_chat_history, state_chat_pairs, state_sources, state_plan, doc_output],
|
| 612 |
)
|
| 613 |
+
# Generate course package (zip)
|
| 614 |
generate_btn.click(
|
| 615 |
generate_package,
|
| 616 |
inputs=[state_plan, state_sources],
|
db.py
CHANGED
|
@@ -155,4 +155,56 @@ def load_chat(chat_key: str) -> list[dict]:
|
|
| 155 |
def soft_delete_message(message_id: int) -> None:
|
| 156 |
"""Mark a message as deleted without removing it."""
|
| 157 |
with get_conn() as conn:
|
| 158 |
-
conn.execute("UPDATE messages SET status='deleted' WHERE id=?", (message_id,))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
def soft_delete_message(message_id: int) -> None:
|
| 156 |
"""Mark a message as deleted without removing it."""
|
| 157 |
with get_conn() as conn:
|
| 158 |
+
conn.execute("UPDATE messages SET status='deleted' WHERE id=?", (message_id,))
|
| 159 |
+
|
| 160 |
+
# ---------------------------------------------------------------------------
|
| 161 |
+
# Additional chat management helpers
|
| 162 |
+
#
|
| 163 |
+
# The following functions support listing, renaming, and deleting entire chat
|
| 164 |
+
# sessions. They complement the existing new_chat(), append_message(), and
|
| 165 |
+
# load_chat() functions above. Each chat is keyed by a unique chat_key.
|
| 166 |
+
|
| 167 |
+
def list_chats(limit: int = 100) -> list[dict]:
|
| 168 |
+
"""Return a list of chat summaries ordered by most recent.
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
limit: Maximum number of chats to return.
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
A list of dictionaries with keys 'key', 'title' and 'created_at'.
|
| 175 |
+
"""
|
| 176 |
+
with get_conn() as conn:
|
| 177 |
+
rows = conn.execute(
|
| 178 |
+
"SELECT chat_key, COALESCE(title, 'Untitled'), created_at FROM chats "
|
| 179 |
+
"ORDER BY created_at DESC LIMIT ?",
|
| 180 |
+
(limit,),
|
| 181 |
+
).fetchall()
|
| 182 |
+
return [
|
| 183 |
+
{"key": chat_key, "title": title, "created_at": created_at}
|
| 184 |
+
for chat_key, title, created_at in rows
|
| 185 |
+
]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def rename_chat(chat_key: str, new_title: str) -> None:
|
| 189 |
+
"""Rename a chat session given its key.
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
chat_key: The unique key of the chat to rename.
|
| 193 |
+
new_title: The new title to set.
|
| 194 |
+
"""
|
| 195 |
+
with get_conn() as conn:
|
| 196 |
+
conn.execute(
|
| 197 |
+
"UPDATE chats SET title=? WHERE chat_key=?",
|
| 198 |
+
(new_title, chat_key),
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def delete_chat(chat_key: str) -> None:
|
| 203 |
+
"""Delete a chat session and all its messages.
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
chat_key: The unique key of the chat to delete.
|
| 207 |
+
"""
|
| 208 |
+
with get_conn() as conn:
|
| 209 |
+
conn.execute("DELETE FROM messages WHERE chat_key=?", (chat_key,))
|
| 210 |
+
conn.execute("DELETE FROM chats WHERE chat_key=?", (chat_key,))
|
docx_utils.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility functions for generating Word documents (DOCX) from course outlines.
|
| 3 |
+
|
| 4 |
+
This module provides a helper to convert a course outline into a .docx file.
|
| 5 |
+
It uses the python-docx library to construct a simple document with a title,
|
| 6 |
+
outline sections, optional bullet points, and a references section. The
|
| 7 |
+
generated file is saved to a temporary location and the path is returned.
|
| 8 |
+
|
| 9 |
+
Dependencies:
|
| 10 |
+
python-docx >= 1.1.0
|
| 11 |
+
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
from typing import List, Dict, Optional
|
| 16 |
+
|
| 17 |
+
from docx import Document
|
| 18 |
+
from docx.shared import Pt
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def outline_to_docx(
|
| 22 |
+
title: str,
|
| 23 |
+
outline_text: str,
|
| 24 |
+
references: Optional[List[Dict[str, str]]] = None,
|
| 25 |
+
path: str = "/tmp/course_outline.docx",
|
| 26 |
+
) -> str:
|
| 27 |
+
"""Create a Word document from a course outline.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
title: Title for the course or outline.
|
| 31 |
+
outline_text: Plain text of the outline. Each line will be written as a
|
| 32 |
+
separate paragraph. If you need more structured output, consider
|
| 33 |
+
extending this function to parse the text.
|
| 34 |
+
references: Optional list of dictionaries with 'title' and 'url' keys.
|
| 35 |
+
These will be included in a references section at the end.
|
| 36 |
+
path: File system path where the document will be written. Defaults
|
| 37 |
+
to '/tmp/course_outline.docx'.
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
The file path to the generated document.
|
| 41 |
+
"""
|
| 42 |
+
# Create a new document
|
| 43 |
+
doc = Document()
|
| 44 |
+
|
| 45 |
+
# Title heading
|
| 46 |
+
title_heading = doc.add_heading(level=1)
|
| 47 |
+
run = title_heading.add_run(title or "Course Outline")
|
| 48 |
+
run.bold = True
|
| 49 |
+
run.font.size = Pt(20)
|
| 50 |
+
|
| 51 |
+
# Write each line of the outline as a paragraph
|
| 52 |
+
for line in (outline_text or "").splitlines():
|
| 53 |
+
line = line.strip()
|
| 54 |
+
if not line:
|
| 55 |
+
continue
|
| 56 |
+
# Use list bullet style if the line looks like a bullet (starts with '-' or '*')
|
| 57 |
+
if line.lstrip().startswith(("-", "*")):
|
| 58 |
+
# Remove leading bullet characters
|
| 59 |
+
content = line.lstrip("-* ")
|
| 60 |
+
p = doc.add_paragraph(content, style="List Bullet")
|
| 61 |
+
else:
|
| 62 |
+
p = doc.add_paragraph(line)
|
| 63 |
+
# Standard font size
|
| 64 |
+
for r in p.runs:
|
| 65 |
+
r.font.size = Pt(12)
|
| 66 |
+
|
| 67 |
+
# References
|
| 68 |
+
if references:
|
| 69 |
+
doc.add_heading("References", level=2)
|
| 70 |
+
for ref in references:
|
| 71 |
+
ref_title = ref.get("title", "")
|
| 72 |
+
ref_url = ref.get("url", "")
|
| 73 |
+
para = doc.add_paragraph()
|
| 74 |
+
if ref_title:
|
| 75 |
+
para.add_run(ref_title).bold = True
|
| 76 |
+
para.add_run(" — ")
|
| 77 |
+
para.add_run(ref_url)
|
| 78 |
+
for r in para.runs:
|
| 79 |
+
r.font.size = Pt(11)
|
| 80 |
+
|
| 81 |
+
# Timestamp
|
| 82 |
+
doc.add_paragraph()
|
| 83 |
+
timestamp = datetime.now().strftime("Generated %Y-%m-%d %H:%M:%S")
|
| 84 |
+
footer = doc.add_paragraph(timestamp)
|
| 85 |
+
for r in footer.runs:
|
| 86 |
+
r.font.size = Pt(8)
|
| 87 |
+
|
| 88 |
+
# Save document
|
| 89 |
+
doc.save(path)
|
| 90 |
+
return path
|
requirements.txt
CHANGED
|
@@ -10,4 +10,6 @@ beautifulsoup4>=4.12.0
|
|
| 10 |
|
| 11 |
# Allow the agent to fetch YouTube video transcripts for summarization
|
| 12 |
youtube-transcript-api>=1.0.0
|
|
|
|
|
|
|
| 13 |
python-docx>=1.1.0
|
|
|
|
| 10 |
|
| 11 |
# Allow the agent to fetch YouTube video transcripts for summarization
|
| 12 |
youtube-transcript-api>=1.0.0
|
| 13 |
+
|
| 14 |
+
# Required to generate Word documents from course outlines
|
| 15 |
python-docx>=1.1.0
|