Add page LLM configuration settings
This commit is contained in:
parent
38e34219d3
commit
8439fdc7ab
67
app.py
67
app.py
@ -253,6 +253,61 @@ async def get_task(task_id: str):
|
|||||||
return task_manager.tasks[task_id]
|
return task_manager.tasks[task_id]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/config/status")
|
||||||
|
async def check_config_status():
|
||||||
|
config_path = Path(__file__).parent / "config" / "config.toml"
|
||||||
|
example_config_path = Path(__file__).parent / "config" / "config.example.toml"
|
||||||
|
|
||||||
|
if config_path.exists():
|
||||||
|
return {"status": "exists"}
|
||||||
|
elif example_config_path.exists():
|
||||||
|
try:
|
||||||
|
with open(example_config_path, "rb") as f:
|
||||||
|
example_config = tomllib.load(f)
|
||||||
|
return {"status": "missing", "example_config": example_config}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
else:
|
||||||
|
return {"status": "no_example"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/config/save")
|
||||||
|
async def save_config(config_data: dict = Body(...)):
|
||||||
|
try:
|
||||||
|
config_dir = Path(__file__).parent / "config"
|
||||||
|
config_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
config_path = config_dir / "config.toml"
|
||||||
|
|
||||||
|
toml_content = ""
|
||||||
|
|
||||||
|
if "llm" in config_data:
|
||||||
|
toml_content += "# Global LLM configuration\n[llm]\n"
|
||||||
|
llm_config = config_data["llm"]
|
||||||
|
for key, value in llm_config.items():
|
||||||
|
if key != "vision":
|
||||||
|
if isinstance(value, str):
|
||||||
|
toml_content += f'{key} = "{value}"\n'
|
||||||
|
else:
|
||||||
|
toml_content += f"{key} = {value}\n"
|
||||||
|
|
||||||
|
if "server" in config_data:
|
||||||
|
toml_content += "\n# Server configuration\n[server]\n"
|
||||||
|
server_config = config_data["server"]
|
||||||
|
for key, value in server_config.items():
|
||||||
|
if isinstance(value, str):
|
||||||
|
toml_content += f'{key} = "{value}"\n'
|
||||||
|
else:
|
||||||
|
toml_content += f"{key} = {value}\n"
|
||||||
|
|
||||||
|
with open(config_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(toml_content)
|
||||||
|
|
||||||
|
return {"status": "success"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(Exception)
|
@app.exception_handler(Exception)
|
||||||
async def generic_exception_handler(request: Request, exc: Exception):
|
async def generic_exception_handler(request: Request, exc: Exception):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -268,18 +323,20 @@ def load_config():
|
|||||||
try:
|
try:
|
||||||
config_path = Path(__file__).parent / "config" / "config.toml"
|
config_path = Path(__file__).parent / "config" / "config.toml"
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
return {"host": "localhost", "port": 5172}
|
||||||
|
|
||||||
with open(config_path, "rb") as f:
|
with open(config_path, "rb") as f:
|
||||||
config = tomllib.load(f)
|
config = tomllib.load(f)
|
||||||
|
|
||||||
return {"host": config["server"]["host"], "port": config["server"]["port"]}
|
return {"host": config["server"]["host"], "port": config["server"]["port"]}
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise RuntimeError(
|
return {"host": "localhost", "port": 5172}
|
||||||
"Configuration file not found, please check if config/fig.toml exists"
|
|
||||||
)
|
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise RuntimeError(
|
print(
|
||||||
f"The configuration file is missing necessary fields: {str(e)}"
|
f"The configuration file is missing necessary fields: {str(e)}, use default configuration"
|
||||||
)
|
)
|
||||||
|
return {"host": "localhost", "port": 5172}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
163
static/main.js
163
static/main.js
@ -1,5 +1,163 @@
|
|||||||
let currentEventSource = null;
|
let currentEventSource = null;
|
||||||
|
|
||||||
|
let exampleApiKey = '';
|
||||||
|
|
||||||
|
function checkConfigStatus() {
|
||||||
|
fetch('/config/status')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'missing') {
|
||||||
|
showConfigModal(data.example_config);
|
||||||
|
} else if (data.status === 'no_example') {
|
||||||
|
alert('Error: Missing configuration example file! Please ensure that the config/config.example.toml file exists.');
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
alert('Configuration check error:' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Configuration check failed:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display configuration pop-up and fill in sample configurations
|
||||||
|
function showConfigModal(exampleConfig) {
|
||||||
|
const configModal = document.getElementById('config-modal');
|
||||||
|
if (!configModal) return;
|
||||||
|
|
||||||
|
configModal.classList.add('active');
|
||||||
|
|
||||||
|
if (exampleConfig) {
|
||||||
|
fillConfigForm(exampleConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveButton = document.getElementById('save-config-btn');
|
||||||
|
if (saveButton) {
|
||||||
|
saveButton.onclick = saveConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use example configuration to fill in the form
|
||||||
|
function fillConfigForm(exampleConfig) {
|
||||||
|
if (exampleConfig.llm) {
|
||||||
|
const llm = exampleConfig.llm;
|
||||||
|
|
||||||
|
setInputValue('llm-model', llm.model);
|
||||||
|
setInputValue('llm-base-url', llm.base_url);
|
||||||
|
setInputValue('llm-api-key', llm.api_key);
|
||||||
|
// Save example API key for later verification
|
||||||
|
exampleApiKey = llm.api_key || '';
|
||||||
|
|
||||||
|
setInputValue('llm-max-tokens', llm.max_tokens);
|
||||||
|
setInputValue('llm-temperature', llm.temperature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exampleConfig.server) {
|
||||||
|
setInputValue('server-host', exampleConfig.server.host);
|
||||||
|
setInputValue('server-port', exampleConfig.server.port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInputValue(id, value) {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (input && value !== undefined) {
|
||||||
|
input.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig() {
|
||||||
|
const configData = collectFormData();
|
||||||
|
|
||||||
|
const requiredFields = [
|
||||||
|
{ id: 'llm-model', name: 'Model Name' },
|
||||||
|
{ id: 'llm-base-url', name: 'API Base URL' },
|
||||||
|
{ id: 'llm-api-key', name: 'API Key' },
|
||||||
|
{ id: 'server-host', name: 'Server Host' },
|
||||||
|
{ id: 'server-port', name: 'Server Port' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let missingFields = [];
|
||||||
|
requiredFields.forEach(field => {
|
||||||
|
if (!document.getElementById(field.id).value.trim()) {
|
||||||
|
missingFields.push(field.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
document.getElementById('config-error').textContent =
|
||||||
|
`Please fill in the necessary configuration information: ${missingFields.join(', ')}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the API key is the same as the example configuration
|
||||||
|
const apiKey = document.getElementById('llm-api-key').value.trim();
|
||||||
|
if (apiKey === exampleApiKey && exampleApiKey.includes('sk-')) {
|
||||||
|
document.getElementById('config-error').textContent =
|
||||||
|
`Please enter your own API key`;
|
||||||
|
// Highlight the API key input box
|
||||||
|
document.getElementById('llm-api-key').parentElement.classList.add('error');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
document.getElementById('llm-api-key').parentElement.classList.remove('error');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send configuration to server
|
||||||
|
fetch('/config/save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(configData)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Close pop-up
|
||||||
|
document.getElementById('config-modal').classList.remove('active');
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert('Configuration saved successfully! The application will use the new configuration on next startup.');
|
||||||
|
|
||||||
|
// Refresh page
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
document.getElementById('config-error').textContent =
|
||||||
|
`Save failed: ${data.message}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
document.getElementById('config-error').textContent =
|
||||||
|
`Request error: ${error.message}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect form data
|
||||||
|
function collectFormData() {
|
||||||
|
const configData = {
|
||||||
|
llm: {
|
||||||
|
model: document.getElementById('llm-model').value,
|
||||||
|
base_url: document.getElementById('llm-base-url').value,
|
||||||
|
api_key: document.getElementById('llm-api-key').value
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: document.getElementById('server-host').value,
|
||||||
|
port: parseInt(document.getElementById('server-port').value || '5172')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add optional fields
|
||||||
|
const maxTokens = document.getElementById('llm-max-tokens').value;
|
||||||
|
if (maxTokens) {
|
||||||
|
configData.llm.max_tokens = parseInt(maxTokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
const temperature = document.getElementById('llm-temperature').value;
|
||||||
|
if (temperature) {
|
||||||
|
configData.llm.temperature = parseFloat(temperature);
|
||||||
|
}
|
||||||
|
|
||||||
|
return configData;
|
||||||
|
}
|
||||||
|
|
||||||
function createTask() {
|
function createTask() {
|
||||||
const promptInput = document.getElementById('prompt-input');
|
const promptInput = document.getElementById('prompt-input');
|
||||||
const prompt = promptInput.value.trim();
|
const prompt = promptInput.value.trim();
|
||||||
@ -495,6 +653,9 @@ print("Hello from Python Simulated environment!")
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Check configuration status
|
||||||
|
checkConfigStatus();
|
||||||
|
|
||||||
loadHistory();
|
loadHistory();
|
||||||
|
|
||||||
document.getElementById('prompt-input').addEventListener('keydown', (e) => {
|
document.getElementById('prompt-input').addEventListener('keydown', (e) => {
|
||||||
@ -535,6 +696,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (pythonModal && pythonModal.classList.contains('active')) {
|
if (pythonModal && pythonModal.classList.contains('active')) {
|
||||||
pythonModal.classList.remove('active');
|
pythonModal.classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do not close the configuration pop-up, because the configuration is required
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
183
static/style.css
183
static/style.css
@ -520,3 +520,186 @@ pre {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 2000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-content {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
width: 80%;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-header {
|
||||||
|
padding: 20px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background-color: var(--info-color);
|
||||||
|
color: white;
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-header h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(90vh - 140px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-modal-footer {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--info-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background-color: var(--info-color);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:hover {
|
||||||
|
background-color: #0069d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
background-color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-error {
|
||||||
|
color: var(--error-color);
|
||||||
|
margin-right: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 8px 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.error input {
|
||||||
|
border-color: var(--error-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.25);
|
||||||
|
background-color: rgba(220, 53, 69, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group.error label {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .error-message {
|
||||||
|
color: var(--error-color);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: block;
|
||||||
|
animation: errorAppear 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes errorAppear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-box {
|
||||||
|
background-color: #fff8e1;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-box p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-box p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-help {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-mark {
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
@ -34,6 +34,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="config-modal" class="config-modal">
|
||||||
|
<div class="config-modal-content">
|
||||||
|
<div class="config-modal-header">
|
||||||
|
<h2>System Configuration</h2>
|
||||||
|
<p>Please fill in the necessary configuration information to continue using the system</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-modal-body">
|
||||||
|
<div class="note-box">
|
||||||
|
<p>⚠️ Please ensure that the following configuration information is correct.</p>
|
||||||
|
<p>If you do not have an API key, please obtain one from the corresponding AI service provider.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-section">
|
||||||
|
<h3>LLM Configuration</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="llm-model">Model Name <span class="required-mark">*</span></label>
|
||||||
|
<input type="text" id="llm-model" name="llm.model" placeholder="例如: claude-3-5-sonnet">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="llm-base-url">API Base URL <span class="required-mark">*</span></label>
|
||||||
|
<input type="text" id="llm-base-url" name="llm.base_url" placeholder="例如: https://api.openai.com/v1">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="llm-api-key">API Key <span class="required-mark">*</span></label>
|
||||||
|
<input type="password" id="llm-api-key" name="llm.api_key" placeholder="Your API key, for example: sk-...">
|
||||||
|
<span class="field-help">Must be your own valid API key, not the placeholder in the example</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="llm-max-tokens">Max Tokens</label>
|
||||||
|
<input type="number" id="llm-max-tokens" name="llm.max_tokens" placeholder="例如: 4096">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="llm-temperature">Temperature</label>
|
||||||
|
<input type="number" id="llm-temperature" name="llm.temperature" step="0.1" placeholder="例如: 0.0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-section">
|
||||||
|
<h3>Server Configuration</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="server-host">Host <span class="required-mark">*</span></label>
|
||||||
|
<input type="text" id="server-host" name="server.host" placeholder="例如: localhost">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="server-port">Port <span class="required-mark">*</span></label>
|
||||||
|
<input type="number" id="server-port" name="server.port" placeholder="例如: 5172">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="config-modal-footer">
|
||||||
|
<p id="config-error" class="config-error"></p>
|
||||||
|
<div class="config-actions">
|
||||||
|
<button id="save-config-btn" class="primary-btn">Save Configuration</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/static/main.js"></script>
|
<script src="/static/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user