Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM python:3.11-slim

WORKDIR /app


RUN apt-get update && apt-get install -y \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*


COPY . .


RUN pip install -e .


EXPOSE 8000


CMD ["python", "-m", "excel_agent.main", "serve"]
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,19 +202,15 @@ Excel_Agent/

## 🐳 Docker 部署

```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install uv && uv sync
EXPOSE 8000
CMD ["uv", "run", "python", "-m", "excel_agent.main", "serve"]
```
### Docker example

```bash
docker build -t excel-agent .
docker run -p 8000:8000 -e OPENAI_API_KEY=your-key excel-agent
```
`mv config.example.yaml /home/docker/excelmind/docker/config.yaml`

`docker build -t excel-agent .`

`docker compose up -d`

visit ip:8000

## 🔧 开发

Expand Down
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
excel-agent:
image: excel-agent:latest
ports:
- "8000:8000"
environment:
- PYTHONPATH=/app/src
- PYTHONUNBUFFERED=1
volumes:
- /home/docker/excelmind/docker/data:/app/data
- /home/docker/excelmind/docker/logs:/app/logs
- /home/docker/excelmind/docker/config.yaml:/app/config.yaml
restart: unless-stopped
91 changes: 89 additions & 2 deletions src/excel_agent/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Any, Dict, List, Optional
from datetime import datetime, date

from fastapi import FastAPI, HTTPException, UploadFile, File, Response
from fastapi import FastAPI, HTTPException, UploadFile, File, Response, Body
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
Expand Down Expand Up @@ -716,6 +716,94 @@ async def reset_knowledge():
return {"success": True, "message": "知识库已重置"}


# ============ 配置管理 API ============

import yaml
import os
import subprocess
import sys


@app.get("/config")
async def get_config_file():
"""获取当前配置文件内容"""
config = get_config()
config_path = config._config_path if hasattr(config, '_config_path') else "config.yaml"

try:
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
content = f.read()
return Response(content=content, media_type="text/yaml")
else:
# 返回默认配置结构
default_config = {
"server": {
"host": "0.0.0.0",
"port": 8000
},
"llm": {
"provider": "openai",
"model": "gpt-3.5-turbo",
"api_key": "YOUR_API_KEY_HERE"
},
"knowledge_base": {
"enabled": True,
"knowledge_dir": "knowledge"
}
}
content = yaml.dump(default_config, allow_unicode=True, sort_keys=False)
return Response(content=content, media_type="text/yaml")
except Exception as e:
raise HTTPException(status_code=500, detail=f"读取配置文件失败: {str(e)}")


@app.post("/config")
async def update_config_file(content: str = Body(..., media_type="text/yaml")):
"""更新配置文件并应用新配置"""
config = get_config()
config_path = config._config_path if hasattr(config, '_config_path') else "config.yaml"

if not content:
raise HTTPException(status_code=400, detail="配置内容不能为空")

try:
# 验证YAML格式
new_config_data = yaml.safe_load(content)

# 备份原文件
backup_path = f"{config_path}.backup"
if os.path.exists(config_path):
import shutil
shutil.copy2(config_path, backup_path)

# 写入新配置
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)

# 重新加载配置
try:
new_config = load_config(config_path)
set_config(new_config)

# 重置相关组件以应用新配置
reset_loader()
reset_graph()

except Exception as e:
# 配置加载失败,恢复备份
if os.path.exists(backup_path):
shutil.copy2(backup_path, config_path)
raise HTTPException(status_code=400, detail=f"配置格式错误: {str(e)}")

return {"success": True, "message": "配置已更新并应用,相关组件已重置"}

except yaml.YAMLError as e:
raise HTTPException(status_code=400, detail=f"YAML格式错误: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"更新配置失败: {str(e)}")


def run_server():
"""运行服务器"""
import uvicorn
Expand All @@ -725,4 +813,3 @@ def run_server():
host=config.server.host,
port=config.server.port,
)

111 changes: 108 additions & 3 deletions src/excel_agent/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1609,9 +1609,12 @@
<aside class="sidebar">
<div class="sidebar-header">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h1>ExcelMind</h1>
<h1>ExcelMind</h1>
<div style="display: flex; gap: 8px;">
<button id="config-toggle" class="theme-toggle-btn" title="配置编辑器">⚙️</button>
<button id="theme-toggle" class="theme-toggle-btn" title="切换主题">🌙</button>
</div>
</div>
<div class="subtitle">Excel 智能数据问答助手</div>
</div>

Expand Down Expand Up @@ -1736,11 +1739,39 @@ <h2 id="kb-modal-title">📚 知识详情</h2>
</div>
</div>

<!-- Configuration Editor Modal -->
<div class="modal-overlay" id="config-modal" style="display: none;">
<div class="modal" style="max-width: 900px; max-height: 80vh;">
<div class="modal-header">
<h2>⚙️ 配置编辑器</h2>
<button class="modal-close" id="close-config">&times;</button>
</div>
<div class="modal-body" style="display: flex; flex-direction: column; height: 60vh;">
<div style="margin-bottom: 16px; color: var(--text-secondary); font-size: 0.9rem;">
编辑 config.yaml 配置文件。保存后应用将自动重启以应用新配置。
</div>
<textarea
id="config-editor"
style="flex: 1; width: 100%; background: var(--bg-tertiary);
color: var(--text-primary); border: 1px solid var(--border);
border-radius: 8px; padding: 12px; font-family: 'Courier New', monospace;
font-size: 0.9rem; resize: none;"
spellcheck="false"
placeholder="加载配置文件中...">
</textarea>
<div class="modal-actions" style="margin-top: 16px;">
<button type="button" class="btn-secondary" id="config-cancel">取消</button>
<button type="button" class="btn-primary" id="save-config">保存并重启</button>
</div>
</div>
</div>
</div>

<!-- Upload Overlay -->
<div class="upload-overlay" id="upload-overlay">
<div class="upload-spinner"></div>
<div class="upload-overlay-text" id="upload-overlay-text">正在上传...</div>
<div class="upload-overlay-sub" id="upload-overlay-sub">请稍候,文件较大时可能需要一些时间</div>
<div class="upload-overlay-sub" id="upload-overlay-sub">请稍候</div>
</div>

<!-- Main Content -->
Expand Down Expand Up @@ -3009,7 +3040,81 @@ <h3>开始对话</h3>

// Load knowledge entries on page load
loadKnowledgeEntries();

// Configuration Editor
const configModal = document.getElementById('config-modal');
const configToggle = document.getElementById('config-toggle');
const configEditor = document.getElementById('config-editor');
const saveConfigBtn = document.getElementById('save-config');
const closeConfigBtn = document.getElementById('close-config');
const cancelConfigBtn = document.getElementById('config-cancel');

configToggle.addEventListener('click', openConfigModal);
closeConfigBtn.addEventListener('click', closeConfigModal);
cancelConfigBtn.addEventListener('click', closeConfigModal);
saveConfigBtn.addEventListener('click', saveConfig);

function openConfigModal() {
loadConfig();
// 显示并添加 show 类以触发可见状态
configModal.style.display = 'flex';
configModal.classList.add('show');
}

function closeConfigModal() {
configModal.classList.remove('show');
configModal.style.display = 'none';
}

async function loadConfig() {
try {
const response = await fetch('/config');
if (response.ok) {
const config = await response.text();
configEditor.value = config;
} else {
configEditor.value = '# 无法加载配置文件';
}
} catch (error) {
configEditor.value = '# 加载配置文件失败: ' + error.message;
}
}

async function saveConfig() {
const configContent = configEditor.value;

try {
const response = await fetch('/config', {
method: 'POST',
headers: { 'Content-Type': 'text/yaml' },
body: configContent
});

if (response.ok) {
const result = await response.json();
alert('配置保存成功!' + result.message);
closeConfigModal();

// 刷新页面以应用新配置
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
const error = await response.json();
throw new Error(error.detail || '保存失败');
}
} catch (error) {
alert('保存配置失败: ' + error.message);
}
}

// Close modal when clicking outside
configModal.addEventListener('click', (e) => {
if (e.target === configModal) {
closeConfigModal();
}
});
</script>
</body>

</html>
</html>
7 changes: 7 additions & 0 deletions src/excel_agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import Any, Dict, List, Optional

import json
import pandas as pd
from langchain_core.tools import tool

Expand Down Expand Up @@ -104,6 +105,12 @@ def filter_data(
df = loader.dataframe.copy()

try:
# 处理 JSON 字符串参数
if isinstance(filters, str):
filters = json.loads(filters)
if isinstance(select_columns, str):
select_columns = json.loads(select_columns)

# 初始掩码为全 True
final_mask = pd.Series([True] * len(df))

Expand Down