mirror of
https://github.com/oobabooga/text-generation-webui.git
synced 2025-06-07 06:06:20 -04:00
Compare commits
7 commits
dc8ed6dbe7
...
9e80193008
Author | SHA1 | Date | |
---|---|---|---|
|
9e80193008 | ||
|
0816ecedb7 | ||
|
98a7508a99 | ||
|
85f2f01a3a | ||
|
f8d220c1e6 | ||
|
4a2727b71d | ||
|
1d88456659 |
24 changed files with 167 additions and 84 deletions
|
@ -16,7 +16,7 @@ Its goal is to become the [AUTOMATIC1111/stable-diffusion-webui](https://github.
|
|||
- Easy setup: Choose between **portable builds** (zero setup, just unzip and run) for GGUF models on Windows/Linux/macOS, or the one-click installer that creates a self-contained `installer_files` directory.
|
||||
- 100% offline and private, with zero telemetry, external resources, or remote update requests.
|
||||
- Automatic prompt formatting using Jinja2 templates. You don't need to ever worry about prompt formats.
|
||||
- **File attachments**: Upload text files and PDF documents to talk about their contents.
|
||||
- **File attachments**: Upload text files, PDF documents, and .docx documents to talk about their contents.
|
||||
- **Web search**: Optionally search the internet with LLM-generated queries to add context to the conversation.
|
||||
- Aesthetic UI with dark and light themes.
|
||||
- `instruct` mode for instruction-following (like ChatGPT), and `chat-instruct`/`chat` modes for talking to custom characters.
|
||||
|
|
57
css/main.css
57
css/main.css
|
@ -582,7 +582,6 @@ div.svelte-362y77>*, div.svelte-362y77>.form>* {
|
|||
|
||||
#chat-input {
|
||||
padding: 0;
|
||||
padding-top: 18px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
@ -661,31 +660,6 @@ div.svelte-362y77>*, div.svelte-362y77>.form>* {
|
|||
}
|
||||
}
|
||||
|
||||
#show-controls {
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
border: 0 !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#show-controls label {
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dark #show-controls span {
|
||||
color: var(--neutral-400);
|
||||
}
|
||||
|
||||
#show-controls span {
|
||||
color: var(--neutral-600);
|
||||
}
|
||||
|
||||
#typing-container {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
@ -785,6 +759,32 @@ div.svelte-362y77>*, div.svelte-362y77>.form>* {
|
|||
background: var(--selected-item-color-dark) !important;
|
||||
}
|
||||
|
||||
#show-controls {
|
||||
height: 36px;
|
||||
border-top: 1px solid var(--border-color-dark) !important;
|
||||
border-left: 1px solid var(--border-color-dark) !important;
|
||||
border-right: 1px solid var(--border-color-dark) !important;
|
||||
border-radius: 0;
|
||||
border-bottom: 0 !important;
|
||||
background-color: var(--darker-gray);
|
||||
padding-top: 3px;
|
||||
padding-left: 4px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#show-controls label {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
font-weight: bold;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
#show-controls label input {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.transparent-substring {
|
||||
opacity: 0.333;
|
||||
}
|
||||
|
@ -1555,3 +1555,8 @@ strong {
|
|||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Fix extra gaps for hidden elements on the right sidebar */
|
||||
.svelte-sa48pu.stretch:has(> .hidden:only-child) {
|
||||
display: none;
|
||||
}
|
||||
|
|
48
js/main.js
48
js/main.js
|
@ -277,7 +277,7 @@ for (i = 0; i < slimDropdownElements.length; i++) {
|
|||
// The show/hide events were adapted from:
|
||||
// https://github.com/SillyTavern/SillyTavern/blob/6c8bd06308c69d51e2eb174541792a870a83d2d6/public/script.js
|
||||
//------------------------------------------------
|
||||
var buttonsInChat = document.querySelectorAll("#chat-tab #chat-buttons button");
|
||||
var buttonsInChat = document.querySelectorAll("#chat-tab #chat-buttons button, #chat-tab #chat-buttons #show-controls");
|
||||
var button = document.getElementById("hover-element-button");
|
||||
var menu = document.getElementById("hover-menu");
|
||||
var istouchscreen = (navigator.maxTouchPoints > 0) || "ontouchstart" in document.documentElement;
|
||||
|
@ -298,18 +298,21 @@ if (buttonsInChat.length > 0) {
|
|||
const thisButton = buttonsInChat[i];
|
||||
menu.appendChild(thisButton);
|
||||
|
||||
thisButton.addEventListener("click", () => {
|
||||
hideMenu();
|
||||
});
|
||||
// Only apply transformations to button elements
|
||||
if (thisButton.tagName.toLowerCase() === "button") {
|
||||
thisButton.addEventListener("click", () => {
|
||||
hideMenu();
|
||||
});
|
||||
|
||||
const buttonText = thisButton.textContent;
|
||||
const matches = buttonText.match(/(\(.*?\))/);
|
||||
const buttonText = thisButton.textContent;
|
||||
const matches = buttonText.match(/(\(.*?\))/);
|
||||
|
||||
if (matches && matches.length > 1) {
|
||||
// Apply the transparent-substring class to the matched substring
|
||||
const substring = matches[1];
|
||||
const newText = buttonText.replace(substring, ` <span class="transparent-substring">${substring.slice(1, -1)}</span>`);
|
||||
thisButton.innerHTML = newText;
|
||||
if (matches && matches.length > 1) {
|
||||
// Apply the transparent-substring class to the matched substring
|
||||
const substring = matches[1];
|
||||
const newText = buttonText.replace(substring, ` <span class="transparent-substring">${substring.slice(1, -1)}</span>`);
|
||||
thisButton.innerHTML = newText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -382,21 +385,10 @@ document.addEventListener("click", function (event) {
|
|||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------
|
||||
// Relocate the "Show controls" checkbox
|
||||
//------------------------------------------------
|
||||
var elementToMove = document.getElementById("show-controls");
|
||||
var parent = elementToMove.parentNode;
|
||||
for (var i = 0; i < 2; i++) {
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
|
||||
parent.insertBefore(elementToMove, parent.firstChild);
|
||||
|
||||
//------------------------------------------------
|
||||
// Position the chat input
|
||||
//------------------------------------------------
|
||||
document.getElementById("show-controls").parentNode.classList.add("chat-input-positioned");
|
||||
document.getElementById("chat-input-row").classList.add("chat-input-positioned");
|
||||
|
||||
//------------------------------------------------
|
||||
// Focus on the chat input
|
||||
|
@ -872,3 +864,13 @@ function navigateLastAssistantMessage(direction) {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
//------------------------------------------------
|
||||
// Tooltips
|
||||
//------------------------------------------------
|
||||
|
||||
// File upload button
|
||||
document.querySelector("#chat-input .upload-button").title = "Upload text files, PDFs, and DOCX documents";
|
||||
|
||||
// Activate web search
|
||||
document.getElementById("web-search").title = "Search the internet with DuckDuckGo";
|
||||
|
|
|
@ -500,6 +500,9 @@ def add_message_attachment(history, row_idx, file_path, is_user=True):
|
|||
# Process PDF file
|
||||
content = extract_pdf_text(path)
|
||||
file_type = "application/pdf"
|
||||
elif file_extension == '.docx':
|
||||
content = extract_docx_text(path)
|
||||
file_type = "application/docx"
|
||||
else:
|
||||
# Default handling for text files
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
|
@ -538,6 +541,53 @@ def extract_pdf_text(pdf_path):
|
|||
return f"[Error extracting PDF text: {str(e)}]"
|
||||
|
||||
|
||||
def extract_docx_text(docx_path):
|
||||
"""
|
||||
Extract text from a .docx file, including headers,
|
||||
body (paragraphs and tables), and footers.
|
||||
"""
|
||||
try:
|
||||
import docx
|
||||
|
||||
doc = docx.Document(docx_path)
|
||||
parts = []
|
||||
|
||||
# 1) Extract non-empty header paragraphs from each section
|
||||
for section in doc.sections:
|
||||
for para in section.header.paragraphs:
|
||||
text = para.text.strip()
|
||||
if text:
|
||||
parts.append(text)
|
||||
|
||||
# 2) Extract body blocks (paragraphs and tables) in document order
|
||||
parent_elm = doc.element.body
|
||||
for child in parent_elm.iterchildren():
|
||||
if isinstance(child, docx.oxml.text.paragraph.CT_P):
|
||||
para = docx.text.paragraph.Paragraph(child, doc)
|
||||
text = para.text.strip()
|
||||
if text:
|
||||
parts.append(text)
|
||||
|
||||
elif isinstance(child, docx.oxml.table.CT_Tbl):
|
||||
table = docx.table.Table(child, doc)
|
||||
for row in table.rows:
|
||||
cells = [cell.text.strip() for cell in row.cells]
|
||||
parts.append("\t".join(cells))
|
||||
|
||||
# 3) Extract non-empty footer paragraphs from each section
|
||||
for section in doc.sections:
|
||||
for para in section.footer.paragraphs:
|
||||
text = para.text.strip()
|
||||
if text:
|
||||
parts.append(text)
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting text from DOCX: {e}")
|
||||
return f"[Error extracting DOCX text: {str(e)}]"
|
||||
|
||||
|
||||
def generate_search_query(user_message, state):
|
||||
"""Generate a search query from user message using the LLM"""
|
||||
# Augment the user message with search instruction
|
||||
|
@ -660,7 +710,7 @@ def chatbot_wrapper(text, state, regenerate=False, _continue=False, loading_mess
|
|||
|
||||
# Add timestamp for assistant's response at the start of generation
|
||||
row_idx = len(output['internal']) - 1
|
||||
update_message_metadata(output['metadata'], "assistant", row_idx, timestamp=get_current_timestamp())
|
||||
update_message_metadata(output['metadata'], "assistant", row_idx, timestamp=get_current_timestamp(), model_name=shared.model_name)
|
||||
|
||||
# Generate
|
||||
reply = None
|
||||
|
|
|
@ -350,12 +350,14 @@ remove_button = f'<button class="footer-button footer-remove-button" title="Remo
|
|||
info_button = f'<button class="footer-button footer-info-button" title="message">{info_svg}</button>'
|
||||
|
||||
|
||||
def format_message_timestamp(history, role, index):
|
||||
def format_message_timestamp(history, role, index, tooltip_include_timestamp=True):
|
||||
"""Get a formatted timestamp HTML span for a message if available"""
|
||||
key = f"{role}_{index}"
|
||||
if 'metadata' in history and key in history['metadata'] and history['metadata'][key].get('timestamp'):
|
||||
timestamp = history['metadata'][key]['timestamp']
|
||||
return f"<span class='timestamp'>{timestamp}</span>"
|
||||
tooltip_text = get_message_tooltip(history, role, index, include_timestamp=tooltip_include_timestamp)
|
||||
title_attr = f' title="{html.escape(tooltip_text)}"' if tooltip_text else ''
|
||||
return f"<span class='timestamp'{title_attr}>{timestamp}</span>"
|
||||
|
||||
return ""
|
||||
|
||||
|
@ -388,6 +390,23 @@ def format_message_attachments(history, role, index):
|
|||
return ""
|
||||
|
||||
|
||||
def get_message_tooltip(history, role, index, include_timestamp=True):
|
||||
"""Get tooltip text combining timestamp and model name for a message"""
|
||||
key = f"{role}_{index}"
|
||||
if 'metadata' not in history or key not in history['metadata']:
|
||||
return ""
|
||||
|
||||
meta = history['metadata'][key]
|
||||
tooltip_parts = []
|
||||
|
||||
if include_timestamp and meta.get('timestamp'):
|
||||
tooltip_parts.append(meta['timestamp'])
|
||||
if meta.get('model_name'):
|
||||
tooltip_parts.append(f"Model: {meta['model_name']}")
|
||||
|
||||
return " | ".join(tooltip_parts)
|
||||
|
||||
|
||||
def get_version_navigation_html(history, i, role):
|
||||
"""Generate simple navigation arrows for message versions"""
|
||||
key = f"{role}_{i}"
|
||||
|
@ -462,15 +481,13 @@ def generate_instruct_html(history):
|
|||
# Create info buttons for timestamps if they exist
|
||||
info_message_user = ""
|
||||
if user_timestamp != "":
|
||||
# Extract the timestamp value from the span
|
||||
user_timestamp_value = user_timestamp.split('>', 1)[1].split('<', 1)[0]
|
||||
info_message_user = info_button.replace("message", user_timestamp_value)
|
||||
tooltip_text = get_message_tooltip(history, "user", i)
|
||||
info_message_user = info_button.replace('title="message"', f'title="{html.escape(tooltip_text)}"')
|
||||
|
||||
info_message_assistant = ""
|
||||
if assistant_timestamp != "":
|
||||
# Extract the timestamp value from the span
|
||||
assistant_timestamp_value = assistant_timestamp.split('>', 1)[1].split('<', 1)[0]
|
||||
info_message_assistant = info_button.replace("message", assistant_timestamp_value)
|
||||
tooltip_text = get_message_tooltip(history, "assistant", i)
|
||||
info_message_assistant = info_button.replace('title="message"', f'title="{html.escape(tooltip_text)}"')
|
||||
|
||||
if converted_visible[0]: # Don't display empty user messages
|
||||
output += (
|
||||
|
@ -521,8 +538,8 @@ def generate_cai_chat_html(history, name1, name2, style, character, reset_cache=
|
|||
converted_visible = [convert_to_markdown_wrapped(entry, message_id=i, use_cache=i != len(history['visible']) - 1) for entry in row_visible]
|
||||
|
||||
# Get timestamps
|
||||
user_timestamp = format_message_timestamp(history, "user", i)
|
||||
assistant_timestamp = format_message_timestamp(history, "assistant", i)
|
||||
user_timestamp = format_message_timestamp(history, "user", i, tooltip_include_timestamp=False)
|
||||
assistant_timestamp = format_message_timestamp(history, "assistant", i, tooltip_include_timestamp=False)
|
||||
|
||||
# Get attachments
|
||||
user_attachments = format_message_attachments(history, "user", i)
|
||||
|
@ -580,15 +597,13 @@ def generate_chat_html(history, name1, name2, reset_cache=False):
|
|||
# Create info buttons for timestamps if they exist
|
||||
info_message_user = ""
|
||||
if user_timestamp != "":
|
||||
# Extract the timestamp value from the span
|
||||
user_timestamp_value = user_timestamp.split('>', 1)[1].split('<', 1)[0]
|
||||
info_message_user = info_button.replace("message", user_timestamp_value)
|
||||
tooltip_text = get_message_tooltip(history, "user", i)
|
||||
info_message_user = info_button.replace('title="message"', f'title="{html.escape(tooltip_text)}"')
|
||||
|
||||
info_message_assistant = ""
|
||||
if assistant_timestamp != "":
|
||||
# Extract the timestamp value from the span
|
||||
assistant_timestamp_value = assistant_timestamp.split('>', 1)[1].split('<', 1)[0]
|
||||
info_message_assistant = info_button.replace("message", assistant_timestamp_value)
|
||||
tooltip_text = get_message_tooltip(history, "assistant", i)
|
||||
info_message_assistant = info_button.replace('title="message"', f'title="{html.escape(tooltip_text)}"')
|
||||
|
||||
if converted_visible[0]: # Don't display empty user messages
|
||||
output += (
|
||||
|
|
|
@ -55,7 +55,6 @@ def create_ui():
|
|||
|
||||
with gr.Column(scale=10, elem_id='chat-input-container'):
|
||||
shared.gradio['textbox'] = gr.MultimodalTextbox(label='', placeholder='Send a message', file_types=['text', '.pdf'], file_count="multiple", elem_id='chat-input', elem_classes=['add_scrollbar'])
|
||||
shared.gradio['show_controls'] = gr.Checkbox(value=shared.settings['show_controls'], label='Show controls (Ctrl+S)', elem_id='show-controls')
|
||||
shared.gradio['typing-dots'] = gr.HTML(value='<div class="typing"><span></span><span class="dot1"></span><span class="dot2"></span></div>', label='typing', elem_id='typing-container')
|
||||
|
||||
with gr.Column(scale=1, elem_id='generate-stop-container'):
|
||||
|
@ -65,21 +64,15 @@ def create_ui():
|
|||
|
||||
# Hover menu buttons
|
||||
with gr.Column(elem_id='chat-buttons'):
|
||||
with gr.Row():
|
||||
shared.gradio['Regenerate'] = gr.Button('Regenerate (Ctrl + Enter)', elem_id='Regenerate')
|
||||
shared.gradio['Continue'] = gr.Button('Continue (Alt + Enter)', elem_id='Continue')
|
||||
shared.gradio['Remove last'] = gr.Button('Remove last reply (Ctrl + Shift + Backspace)', elem_id='Remove-last')
|
||||
|
||||
with gr.Row():
|
||||
shared.gradio['Impersonate'] = gr.Button('Impersonate (Ctrl + Shift + M)', elem_id='Impersonate')
|
||||
|
||||
with gr.Row():
|
||||
shared.gradio['Send dummy message'] = gr.Button('Send dummy message')
|
||||
shared.gradio['Send dummy reply'] = gr.Button('Send dummy reply')
|
||||
|
||||
with gr.Row():
|
||||
shared.gradio['send-chat-to-default'] = gr.Button('Send to Default')
|
||||
shared.gradio['send-chat-to-notebook'] = gr.Button('Send to Notebook')
|
||||
shared.gradio['Regenerate'] = gr.Button('Regenerate (Ctrl + Enter)', elem_id='Regenerate')
|
||||
shared.gradio['Continue'] = gr.Button('Continue (Alt + Enter)', elem_id='Continue')
|
||||
shared.gradio['Remove last'] = gr.Button('Remove last reply (Ctrl + Shift + Backspace)', elem_id='Remove-last')
|
||||
shared.gradio['Impersonate'] = gr.Button('Impersonate (Ctrl + Shift + M)', elem_id='Impersonate')
|
||||
shared.gradio['Send dummy message'] = gr.Button('Send dummy message')
|
||||
shared.gradio['Send dummy reply'] = gr.Button('Send dummy reply')
|
||||
shared.gradio['send-chat-to-default'] = gr.Button('Send to Default')
|
||||
shared.gradio['send-chat-to-notebook'] = gr.Button('Send to Notebook')
|
||||
shared.gradio['show_controls'] = gr.Checkbox(value=shared.settings['show_controls'], label='Show controls (Ctrl+S)', elem_id='show-controls')
|
||||
|
||||
with gr.Row(elem_id='chat-controls', elem_classes=['pretty_scrollbar']):
|
||||
with gr.Column():
|
||||
|
@ -87,7 +80,7 @@ def create_ui():
|
|||
shared.gradio['start_with'] = gr.Textbox(label='Start reply with', placeholder='Sure thing!', value=shared.settings['start_with'], elem_classes=['add_scrollbar'])
|
||||
|
||||
with gr.Row():
|
||||
shared.gradio['enable_web_search'] = gr.Checkbox(value=shared.settings.get('enable_web_search', False), label='Activate web search')
|
||||
shared.gradio['enable_web_search'] = gr.Checkbox(value=shared.settings.get('enable_web_search', False), label='Activate web search', elem_id='web-search')
|
||||
|
||||
with gr.Row(visible=shared.settings.get('enable_web_search', False)) as shared.gradio['web_search_row']:
|
||||
shared.gradio['web_search_pages'] = gr.Number(value=shared.settings.get('web_search_pages', 3), precision=0, label='Number of pages to download', minimum=1, maximum=10)
|
||||
|
|
|
@ -16,6 +16,7 @@ Pillow>=9.5.0
|
|||
psutil
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -15,6 +15,7 @@ Pillow>=9.5.0
|
|||
psutil
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -15,6 +15,7 @@ Pillow>=9.5.0
|
|||
psutil
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -15,6 +15,7 @@ Pillow>=9.5.0
|
|||
psutil
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -15,6 +15,7 @@ Pillow>=9.5.0
|
|||
psutil
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -15,6 +15,7 @@ Pillow>=9.5.0
|
|||
psutil
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -15,6 +15,7 @@ Pillow>=9.5.0
|
|||
psutil
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -16,6 +16,7 @@ Pillow>=9.5.0
|
|||
psutil
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -15,6 +15,7 @@ Pillow>=9.5.0
|
|||
psutil
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -7,6 +7,7 @@ markdown
|
|||
numpy==1.26.*
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -7,6 +7,7 @@ markdown
|
|||
numpy==1.26.*
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -7,6 +7,7 @@ markdown
|
|||
numpy==1.26.*
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -7,6 +7,7 @@ markdown
|
|||
numpy==1.26.*
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -7,6 +7,7 @@ markdown
|
|||
numpy==1.26.*
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -7,6 +7,7 @@ markdown
|
|||
numpy==1.26.*
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -7,6 +7,7 @@ markdown
|
|||
numpy==1.26.*
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -7,6 +7,7 @@ markdown
|
|||
numpy==1.26.*
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
|
@ -7,6 +7,7 @@ markdown
|
|||
numpy==1.26.*
|
||||
pydantic==2.8.2
|
||||
PyPDF2==3.0.1
|
||||
python-docx==1.1.2
|
||||
pyyaml
|
||||
requests
|
||||
rich
|
||||
|
|
Loading…
Add table
Reference in a new issue