mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
Update package.json
This commit is contained in:
parent
8a15949b84
commit
b19dda393b
BIN
.serena/cache/python/document_symbols_cache_v20-05-25.pkl
vendored
Normal file
BIN
.serena/cache/python/document_symbols_cache_v20-05-25.pkl
vendored
Normal file
Binary file not shown.
68
.serena/memories/code_structure.md
Normal file
68
.serena/memories/code_structure.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Code Structure and Organization
|
||||||
|
|
||||||
|
## Backend Structure (`backend/open_webui/`)
|
||||||
|
|
||||||
|
- **`main.py`**: Main FastAPI application entry point
|
||||||
|
- **`config.py`**: Configuration management and environment variables
|
||||||
|
- **`env.py`**: Environment setup and constants
|
||||||
|
- **`constants.py`**: Application constants and message templates
|
||||||
|
- **`functions.py`**: Function execution and management
|
||||||
|
- **`tasks.py`**: Background task management
|
||||||
|
|
||||||
|
## Router Organization (`backend/open_webui/routers/`)
|
||||||
|
|
||||||
|
Each router handles a specific domain:
|
||||||
|
|
||||||
|
- **`auths.py`**: Authentication and authorization
|
||||||
|
- **`users.py`**: User management
|
||||||
|
- **`chats.py`**: Chat conversations
|
||||||
|
- **`models.py`**: AI model management
|
||||||
|
- **`prompts.py`**: Prompt templates
|
||||||
|
- **`tools.py`**: Tool management
|
||||||
|
- **`functions.py`**: Function management
|
||||||
|
- **`files.py`**: File upload/management
|
||||||
|
- **`images.py`**: Image generation
|
||||||
|
- **`audio.py`**: Speech-to-text and text-to-speech
|
||||||
|
- **`retrieval.py`**: RAG and document processing
|
||||||
|
- **`memories.py`**: Memory management
|
||||||
|
- **`knowledge.py`**: Knowledge base management
|
||||||
|
- **`ollama.py`**: Ollama integration
|
||||||
|
- **`openai.py`**: OpenAI API integration
|
||||||
|
- **`pipelines.py`**: Pipeline management
|
||||||
|
- **`configs.py`**: Configuration management
|
||||||
|
|
||||||
|
## Database Models (`backend/open_webui/models/`)
|
||||||
|
|
||||||
|
- **`users.py`**: User model and settings
|
||||||
|
- **`chats.py`**: Chat conversations
|
||||||
|
- **`models.py`**: AI model definitions
|
||||||
|
- **`files.py`**: File metadata
|
||||||
|
- **`auths.py`**: Authentication data
|
||||||
|
- **`prompts.py`**: Prompt templates
|
||||||
|
- **`tools.py`**: Tool definitions
|
||||||
|
- **`functions.py`**: Function definitions
|
||||||
|
- **`memories.py`**: Memory storage
|
||||||
|
- **`knowledge.py`**: Knowledge base
|
||||||
|
- **`channels.py`**: Communication channels
|
||||||
|
- **`folders.py`**: Organization folders
|
||||||
|
- **`feedbacks.py`**: User feedback
|
||||||
|
|
||||||
|
## Frontend Structure (`src/`)
|
||||||
|
|
||||||
|
- **`app.html`**: Main HTML template
|
||||||
|
- **`app.css`**: Global styles
|
||||||
|
- **`lib/`**: Reusable components and utilities
|
||||||
|
- **`routes/`**: SvelteKit page routes
|
||||||
|
|
||||||
|
## Utilities (`backend/open_webui/utils/`)
|
||||||
|
|
||||||
|
- **`auth.py`**: Authentication utilities
|
||||||
|
- **`misc.py`**: General utilities
|
||||||
|
- **`models.py`**: Model utilities
|
||||||
|
- **`chat.py`**: Chat processing
|
||||||
|
- **`middleware.py`**: Request/response processing
|
||||||
|
- **`tools.py`**: Tool execution
|
||||||
|
- **`embeddings.py`**: Embedding generation
|
||||||
|
- **`code_interpreter.py`**: Code execution
|
||||||
|
- **`filter.py`**: Content filtering
|
||||||
|
- **`plugin.py`**: Plugin management
|
66
.serena/memories/code_style_conventions.md
Normal file
66
.serena/memories/code_style_conventions.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Code Style and Conventions
|
||||||
|
|
||||||
|
## Python Backend Style
|
||||||
|
|
||||||
|
- **Formatter**: Black with default settings
|
||||||
|
- **Linter**: Pylint
|
||||||
|
- **Type Hints**: Strongly encouraged, especially for function signatures
|
||||||
|
- **Docstrings**: Use for public APIs and complex functions
|
||||||
|
- **Import Organization**: Follow PEP 8 standards
|
||||||
|
- **Variable Naming**: snake_case for variables and functions, PascalCase for classes
|
||||||
|
- **Constants**: UPPER_CASE for module-level constants
|
||||||
|
|
||||||
|
## Code Quality Standards
|
||||||
|
|
||||||
|
- **Line Length**: Black default (88 characters)
|
||||||
|
- **String Quotes**: Black will standardize (double quotes preferred)
|
||||||
|
- **Trailing Commas**: Black handles automatically
|
||||||
|
- **Function Organization**: Keep functions focused and single-purpose
|
||||||
|
- **Error Handling**: Use proper exception handling with specific exception types
|
||||||
|
|
||||||
|
## API Design Patterns
|
||||||
|
|
||||||
|
- **FastAPI Routers**: Organize endpoints by domain (users, chats, models, etc.)
|
||||||
|
- **Pydantic Models**: Use for request/response validation
|
||||||
|
- **Response Models**: Consistent JSON structure with proper HTTP status codes
|
||||||
|
- **Authentication**: JWT-based with dependency injection
|
||||||
|
- **Database Models**: SQLAlchemy ORM with proper relationships
|
||||||
|
|
||||||
|
## Frontend Style
|
||||||
|
|
||||||
|
- **Framework**: SvelteKit with TypeScript
|
||||||
|
- **Styling**: Tailwind CSS utility classes
|
||||||
|
- **Component Organization**: Modular components in `src/lib/`
|
||||||
|
- **State Management**: Svelte stores for global state
|
||||||
|
- **Type Safety**: TypeScript throughout the frontend
|
||||||
|
|
||||||
|
## Configuration Management
|
||||||
|
|
||||||
|
- **Environment Variables**: Extensive use of env vars for configuration
|
||||||
|
- **Default Values**: Sensible defaults in `config.py`
|
||||||
|
- **Validation**: Pydantic for configuration validation
|
||||||
|
- **Documentation**: Document all configuration options
|
||||||
|
|
||||||
|
## Database Design
|
||||||
|
|
||||||
|
- **Migrations**: Alembic for database schema changes
|
||||||
|
- **Relationships**: Proper foreign keys and relationships
|
||||||
|
- **Indexes**: Strategic indexing for performance
|
||||||
|
- **Naming**: Descriptive table and column names
|
||||||
|
|
||||||
|
## Security Practices
|
||||||
|
|
||||||
|
- **Authentication**: JWT tokens with proper expiration
|
||||||
|
- **Authorization**: Role-based access control
|
||||||
|
- **Input Validation**: Pydantic models for all inputs
|
||||||
|
- **SQL Injection**: SQLAlchemy ORM prevents direct SQL
|
||||||
|
- **CORS**: Proper CORS configuration
|
||||||
|
- **Environment Secrets**: Never commit secrets to version control
|
||||||
|
|
||||||
|
## Testing Conventions
|
||||||
|
|
||||||
|
- **Backend Tests**: Pytest with fixtures
|
||||||
|
- **Frontend Tests**: Vitest for unit tests
|
||||||
|
- **E2E Tests**: Cypress for integration testing
|
||||||
|
- **Test Organization**: Mirror source code structure
|
||||||
|
- **Mocking**: Mock external dependencies in tests
|
124
.serena/memories/macos_development_guide.md
Normal file
124
.serena/memories/macos_development_guide.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# macOS Development Environment Setup
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
- **Operating System**: macOS
|
||||||
|
- **Python**: 3.11+ (required for backend)
|
||||||
|
- **Node.js**: 18.13.0+ (required for frontend)
|
||||||
|
- **Package Managers**: npm 6.0.0+, optionally uv for Python
|
||||||
|
|
||||||
|
## macOS Specific Commands
|
||||||
|
|
||||||
|
### System Information
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check macOS version
|
||||||
|
sw_vers
|
||||||
|
|
||||||
|
# Check available memory
|
||||||
|
vm_stat | head -5
|
||||||
|
|
||||||
|
# Check disk space
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# Check CPU information
|
||||||
|
sysctl -n machdep.cpu.brand_string
|
||||||
|
|
||||||
|
# Check running processes
|
||||||
|
ps aux | grep open-webui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Homebrew (if not installed)
|
||||||
|
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
|
|
||||||
|
# Install Python with Homebrew
|
||||||
|
brew install python@3.11
|
||||||
|
|
||||||
|
# Install Node.js with Homebrew
|
||||||
|
brew install node
|
||||||
|
|
||||||
|
# Install Docker Desktop for Mac
|
||||||
|
brew install --cask docker
|
||||||
|
|
||||||
|
# Install uv (modern Python package manager)
|
||||||
|
brew install uv
|
||||||
|
```
|
||||||
|
|
||||||
|
### File System Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to project directory
|
||||||
|
cd /path/to/open-webui-next
|
||||||
|
|
||||||
|
# Find files
|
||||||
|
find . -name "*.py" -type f # Find Python files
|
||||||
|
find . -name "*.svelte" -type f # Find Svelte files
|
||||||
|
|
||||||
|
# Search in files
|
||||||
|
grep -r "search_term" backend/ # Search in backend
|
||||||
|
grep -r "search_term" src/ # Search in frontend
|
||||||
|
|
||||||
|
# File permissions
|
||||||
|
chmod +x backend/start.sh # Make script executable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network and Ports
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if port is in use
|
||||||
|
lsof -i :8080 # Check port 8080
|
||||||
|
lsof -i :3000 # Check port 3000 (frontend dev)
|
||||||
|
|
||||||
|
# Kill process on port
|
||||||
|
kill -9 $(lsof -ti:8080) # Kill process on port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python virtual environment
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
deactivate
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
export OPENAI_API_KEY="your-key"
|
||||||
|
echo $OPENAI_API_KEY
|
||||||
|
printenv | grep WEBUI
|
||||||
|
```
|
||||||
|
|
||||||
|
### Troubleshooting Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Python installation
|
||||||
|
which python3
|
||||||
|
python3 --version
|
||||||
|
|
||||||
|
# Check Node.js installation
|
||||||
|
which node
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
# Check Docker
|
||||||
|
docker --version
|
||||||
|
docker ps
|
||||||
|
docker images
|
||||||
|
|
||||||
|
# Clear npm cache
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
|
# Clear Python cache
|
||||||
|
find . -type d -name "__pycache__" -delete
|
||||||
|
find . -name "*.pyc" -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow on macOS
|
||||||
|
|
||||||
|
1. Use Terminal or iTerm2 for command line operations
|
||||||
|
2. Consider using VS Code or PyCharm for development
|
||||||
|
3. Use Docker Desktop for containerized development
|
||||||
|
4. Monitor system resources with Activity Monitor
|
||||||
|
5. Use Homebrew for package management
|
46
.serena/memories/project_overview.md
Normal file
46
.serena/memories/project_overview.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Open WebUI Project Overview
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Open WebUI is an extensible, feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline. It supports various LLM runners like Ollama and OpenAI-compatible APIs, with built-in inference engine for RAG, making it a powerful AI deployment solution.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- Effortless setup with Docker or Kubernetes
|
||||||
|
- Ollama/OpenAI API integration
|
||||||
|
- Granular permissions and user groups
|
||||||
|
- Responsive design with PWA support
|
||||||
|
- Full Markdown and LaTeX support
|
||||||
|
- Voice/video call functionality
|
||||||
|
- Model builder for custom models
|
||||||
|
- Native Python function calling
|
||||||
|
- Local RAG integration
|
||||||
|
- Web search capabilities
|
||||||
|
- Image generation integration
|
||||||
|
- Multi-model conversations
|
||||||
|
- Role-based access control (RBAC)
|
||||||
|
- Multilingual support
|
||||||
|
- Plugin framework with Pipelines
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Backend**: Python 3.11+ with FastAPI
|
||||||
|
- **Frontend**: SvelteKit with TypeScript
|
||||||
|
- **Database**: SQLAlchemy with support for PostgreSQL, MySQL, SQLite
|
||||||
|
- **Vector Database**: Chroma, Milvus, Qdrant, OpenSearch, Elasticsearch, PGVector, Pinecone
|
||||||
|
- **Deployment**: Docker, Kubernetes
|
||||||
|
- **Build Tools**: Vite, Node.js
|
||||||
|
- **Styling**: Tailwind CSS
|
||||||
|
- **Testing**: Pytest (backend), Vitest (frontend), Cypress (e2e)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
The project follows a modern full-stack architecture:
|
||||||
|
|
||||||
|
- **Backend**: Python FastAPI application serving REST APIs and WebSocket connections
|
||||||
|
- **Frontend**: SvelteKit SPA that communicates with the backend APIs
|
||||||
|
- **Database Layer**: SQLAlchemy ORM with Alembic migrations
|
||||||
|
- **Vector Storage**: Pluggable vector database support for RAG functionality
|
||||||
|
- **Authentication**: JWT-based authentication with OAuth support
|
||||||
|
- **Real-time**: WebSocket support for live features
|
||||||
|
- **File Storage**: Configurable storage providers (Local, S3, GCS, Azure)
|
111
.serena/memories/suggested_commands.md
Normal file
111
.serena/memories/suggested_commands.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# Development Commands and Scripts
|
||||||
|
|
||||||
|
## Essential Commands for Development
|
||||||
|
|
||||||
|
### Backend Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pip install -r backend/requirements.txt
|
||||||
|
|
||||||
|
# Run backend in development mode
|
||||||
|
cd backend && python -m uvicorn open_webui.main:app --host 0.0.0.0 --port 8080 --reload
|
||||||
|
|
||||||
|
# Run with uv (modern Python package manager)
|
||||||
|
cd backend && uv run uvicorn open_webui.main:app --host 0.0.0.0 --port 8080 --reload
|
||||||
|
|
||||||
|
# Database migrations
|
||||||
|
alembic upgrade head
|
||||||
|
alembic revision --autogenerate -m "description"
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pytest backend/
|
||||||
|
|
||||||
|
# Code formatting
|
||||||
|
black . --exclude ".venv/|/venv/"
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
pylint backend/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Development server
|
||||||
|
npm run dev
|
||||||
|
npm run dev:5050 # Run on port 5050
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Build with watch mode
|
||||||
|
npm run build:watch
|
||||||
|
|
||||||
|
# Preview production build
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
npm run check
|
||||||
|
npm run check:watch
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
npm run lint:frontend
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# Prepare Pyodide
|
||||||
|
npm run pyodide:fetch
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
npm run i18n:parse
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full Stack Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format both frontend and backend
|
||||||
|
npm run format && npm run format:backend
|
||||||
|
|
||||||
|
# Lint everything
|
||||||
|
npm run lint # Runs lint:frontend, lint:types, lint:backend
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
npm run test:frontend
|
||||||
|
pytest backend/ # Backend tests
|
||||||
|
|
||||||
|
# End-to-end testing
|
||||||
|
npm run cy:open
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Makefile
|
||||||
|
make install # docker-compose up -d
|
||||||
|
make start # docker-compose start
|
||||||
|
make stop # docker-compose stop
|
||||||
|
make startAndBuild # docker-compose up -d --build
|
||||||
|
make update # Update and rebuild
|
||||||
|
|
||||||
|
# Direct docker-compose
|
||||||
|
docker-compose up -d
|
||||||
|
docker-compose up -d --build
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Reset database
|
||||||
|
rm backend/data/webui.db # For SQLite
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
cd backend && alembic upgrade head
|
||||||
|
|
||||||
|
# Create new migration
|
||||||
|
cd backend && alembic revision --autogenerate -m "migration description"
|
||||||
|
```
|
106
.serena/memories/task_completion_workflow.md
Normal file
106
.serena/memories/task_completion_workflow.md
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# Task Completion Workflow
|
||||||
|
|
||||||
|
## When a Development Task is Completed
|
||||||
|
|
||||||
|
### 1. Code Quality Checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Format code
|
||||||
|
npm run format # Frontend formatting
|
||||||
|
npm run format:backend # Backend formatting (Black)
|
||||||
|
|
||||||
|
# Lint code
|
||||||
|
npm run lint # Full linting (frontend + backend)
|
||||||
|
pylint backend/ # Backend specific linting
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
npm run check # TypeScript type checking
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run unit tests
|
||||||
|
npm run test:frontend # Frontend tests with Vitest
|
||||||
|
pytest backend/ # Backend tests with Pytest
|
||||||
|
|
||||||
|
# Run integration tests (if applicable)
|
||||||
|
npm run cy:open # Cypress e2e tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test production build
|
||||||
|
npm run build # Build frontend
|
||||||
|
npm run preview # Preview production build
|
||||||
|
|
||||||
|
# Test backend startup
|
||||||
|
cd backend && python -m uvicorn open_webui.main:app --host 0.0.0.0 --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Database Migrations (if schema changed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate migration if database models were modified
|
||||||
|
cd backend && alembic revision --autogenerate -m "description of changes"
|
||||||
|
|
||||||
|
# Apply migrations
|
||||||
|
cd backend && alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Documentation Updates
|
||||||
|
|
||||||
|
- Update README.md if new features added
|
||||||
|
- Update API documentation if endpoints changed
|
||||||
|
- Update configuration documentation if new env vars added
|
||||||
|
- Update CHANGELOG.md following semantic versioning
|
||||||
|
|
||||||
|
### 6. Git Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stage changes
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Commit with descriptive message
|
||||||
|
git commit -m "feat: add new feature description"
|
||||||
|
# or
|
||||||
|
git commit -m "fix: resolve bug description"
|
||||||
|
# or
|
||||||
|
git commit -m "docs: update documentation"
|
||||||
|
|
||||||
|
# Push changes
|
||||||
|
git push origin feature-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. System Verification Commands (macOS)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check system resources
|
||||||
|
ps aux | grep open-webui # Check if processes are running
|
||||||
|
lsof -i :8080 # Check if port is in use
|
||||||
|
df -h # Check disk space
|
||||||
|
free -m # Check memory usage (if available)
|
||||||
|
|
||||||
|
# Docker verification (if using Docker)
|
||||||
|
docker ps # Check running containers
|
||||||
|
docker logs open-webui # Check container logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Performance Verification
|
||||||
|
|
||||||
|
- Check application startup time
|
||||||
|
- Verify API response times
|
||||||
|
- Test memory usage under load
|
||||||
|
- Verify frontend bundle sizes are reasonable
|
||||||
|
|
||||||
|
### 9. Pre-deployment Checklist
|
||||||
|
|
||||||
|
- [ ] All tests passing
|
||||||
|
- [ ] Code properly formatted and linted
|
||||||
|
- [ ] Documentation updated
|
||||||
|
- [ ] Environment variables documented
|
||||||
|
- [ ] Database migrations tested
|
||||||
|
- [ ] No secrets in code
|
||||||
|
- [ ] Performance is acceptable
|
||||||
|
- [ ] Security considerations addressed
|
66
.serena/project.yml
Normal file
66
.serena/project.yml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# language of the project (csharp, python, rust, java, typescript, javascript, go, cpp, or ruby)
|
||||||
|
# Special requirements:
|
||||||
|
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
language: python
|
||||||
|
|
||||||
|
# whether to use the project's gitignore file to ignore files
|
||||||
|
# Added on 2025-04-07
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
# list of additional paths to ignore
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||||
|
# Added (renamed)on 2025-04-07
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
project_name: "open-webui-next"
|
@ -63,7 +63,7 @@ async def execute_code(
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
|
request.app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT,
|
||||||
form_data.chat_id # Pass chat_id to the enhanced function
|
form_data.chat_id, # Pass chat_id to the enhanced function
|
||||||
)
|
)
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
@ -10,6 +10,7 @@ import websockets
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
# Import necessary models for chat and file operations
|
# Import necessary models for chat and file operations
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
from open_webui.models.files import Files
|
from open_webui.models.files import Files
|
||||||
@ -50,13 +51,15 @@ def get_attached_files_from_chat(chat_id: str) -> List[Dict[str, Any]]:
|
|||||||
"type": file_info.get("type", "file"),
|
"type": file_info.get("type", "file"),
|
||||||
"size": file_info.get("size"),
|
"size": file_info.get("size"),
|
||||||
"url": file_info.get("url"),
|
"url": file_info.get("url"),
|
||||||
"message_id": message_id
|
"message_id": message_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only include files with valid IDs
|
# Only include files with valid IDs
|
||||||
if file_data["id"]:
|
if file_data["id"]:
|
||||||
attached_files.append(file_data)
|
attached_files.append(file_data)
|
||||||
logger.debug(f"Found attached file: {file_data['name']} (ID: {file_data['id']})")
|
logger.debug(
|
||||||
|
f"Found attached file: {file_data['name']} (ID: {file_data['id']})"
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"Found {len(attached_files)} attached files in chat {chat_id}")
|
logger.info(f"Found {len(attached_files)} attached files in chat {chat_id}")
|
||||||
return attached_files
|
return attached_files
|
||||||
@ -66,7 +69,9 @@ def get_attached_files_from_chat(chat_id: str) -> List[Dict[str, Any]]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def auto_prepare_chat_files(chat_id: str, data_dir: str = "data") -> Dict[str, Any]:
|
async def auto_prepare_chat_files(
|
||||||
|
chat_id: str, data_dir: str = "data"
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Automatically prepare files attached to chat messages for use in the Jupyter environment.
|
Automatically prepare files attached to chat messages for use in the Jupyter environment.
|
||||||
Creates symbolic links in the Jupyter data directory pointing to the uploaded files.
|
Creates symbolic links in the Jupyter data directory pointing to the uploaded files.
|
||||||
@ -88,7 +93,7 @@ async def auto_prepare_chat_files(chat_id: str, data_dir: str = "data") -> Dict[
|
|||||||
"skipped_files": [],
|
"skipped_files": [],
|
||||||
"errors": [],
|
"errors": [],
|
||||||
"total_files": 0,
|
"total_files": 0,
|
||||||
"method": None # Will be "symlink" or "copy"
|
"method": None, # Will be "symlink" or "copy"
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -112,7 +117,9 @@ async def auto_prepare_chat_files(chat_id: str, data_dir: str = "data") -> Dict[
|
|||||||
# use_symlinks = await _test_symlink_accessibility(chat_data_dir, data_dir)
|
# use_symlinks = await _test_symlink_accessibility(chat_data_dir, data_dir)
|
||||||
method = "symlink" if use_symlinks else "copy"
|
method = "symlink" if use_symlinks else "copy"
|
||||||
result["method"] = method
|
result["method"] = method
|
||||||
logger.info(f"Using {method} method for file preparation (hardcoded for Docker compatibility)")
|
logger.info(
|
||||||
|
f"Using {method} method for file preparation (hardcoded for Docker compatibility)"
|
||||||
|
)
|
||||||
|
|
||||||
# Track successfully processed files to avoid duplicates
|
# Track successfully processed files to avoid duplicates
|
||||||
processed_file_ids = set()
|
processed_file_ids = set()
|
||||||
@ -125,28 +132,31 @@ async def auto_prepare_chat_files(chat_id: str, data_dir: str = "data") -> Dict[
|
|||||||
# Skip if already processed (deduplication)
|
# Skip if already processed (deduplication)
|
||||||
if file_id in processed_file_ids:
|
if file_id in processed_file_ids:
|
||||||
logger.debug(f"Skipping duplicate file {file_name} (ID: {file_id})")
|
logger.debug(f"Skipping duplicate file {file_name} (ID: {file_id})")
|
||||||
result["skipped_files"].append({
|
result["skipped_files"].append(
|
||||||
"name": file_name,
|
{"name": file_name, "id": file_id, "reason": "duplicate"}
|
||||||
"id": file_id,
|
)
|
||||||
"reason": "duplicate"
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get file from database
|
# Get file from database
|
||||||
file_record = Files.get_file_by_id(file_id)
|
file_record = Files.get_file_by_id(file_id)
|
||||||
if not file_record:
|
if not file_record:
|
||||||
logger.warning(f"File record not found for ID: {file_id}")
|
logger.warning(f"File record not found for ID: {file_id}")
|
||||||
result["errors"].append(f"File record not found: {file_name} (ID: {file_id})")
|
result["errors"].append(
|
||||||
|
f"File record not found: {file_name} (ID: {file_id})"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Use the actual file path from the database
|
# Use the actual file path from the database
|
||||||
if not file_record.path:
|
if not file_record.path:
|
||||||
logger.warning(f"File path not found in record for ID: {file_id}")
|
logger.warning(f"File path not found in record for ID: {file_id}")
|
||||||
result["errors"].append(f"File path not found: {file_name} (ID: {file_id})")
|
result["errors"].append(
|
||||||
|
f"File path not found: {file_name} (ID: {file_id})"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get the actual file path (handles different storage providers)
|
# Get the actual file path (handles different storage providers)
|
||||||
from open_webui.storage.provider import Storage
|
from open_webui.storage.provider import Storage
|
||||||
|
|
||||||
source_file_path = Storage.get_file(file_record.path)
|
source_file_path = Storage.get_file(file_record.path)
|
||||||
|
|
||||||
# Check if source file exists
|
# Check if source file exists
|
||||||
@ -172,23 +182,28 @@ async def auto_prepare_chat_files(chat_id: str, data_dir: str = "data") -> Dict[
|
|||||||
# Create symbolic link using absolute path to ensure it resolves correctly
|
# Create symbolic link using absolute path to ensure it resolves correctly
|
||||||
source_file_path_abs = os.path.abspath(source_file_path)
|
source_file_path_abs = os.path.abspath(source_file_path)
|
||||||
os.symlink(source_file_path_abs, target_path)
|
os.symlink(source_file_path_abs, target_path)
|
||||||
logger.info(f"Created symlink: {target_path} -> {source_file_path_abs}")
|
logger.info(
|
||||||
|
f"Created symlink: {target_path} -> {source_file_path_abs}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Copy file
|
# Copy file
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
shutil.copy2(source_file_path, target_path)
|
shutil.copy2(source_file_path, target_path)
|
||||||
logger.info(f"Copied file: {source_file_path} -> {target_path}")
|
logger.info(f"Copied file: {source_file_path} -> {target_path}")
|
||||||
|
|
||||||
# Record successful preparation
|
# Record successful preparation
|
||||||
result["prepared_files"].append({
|
result["prepared_files"].append(
|
||||||
|
{
|
||||||
"name": file_name,
|
"name": file_name,
|
||||||
"id": file_id,
|
"id": file_id,
|
||||||
"target_path": target_path,
|
"target_path": target_path,
|
||||||
"source_path": source_file_path,
|
"source_path": source_file_path,
|
||||||
"size": file_info.get("size"),
|
"size": file_info.get("size"),
|
||||||
"type": file_info.get("type"),
|
"type": file_info.get("type"),
|
||||||
"method": method
|
"method": method,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
processed_file_ids.add(file_id)
|
processed_file_ids.add(file_id)
|
||||||
|
|
||||||
@ -198,12 +213,16 @@ async def auto_prepare_chat_files(chat_id: str, data_dir: str = "data") -> Dict[
|
|||||||
result["errors"].append(error_msg)
|
result["errors"].append(error_msg)
|
||||||
|
|
||||||
# Set success if we prepared at least some files or if there were no errors
|
# Set success if we prepared at least some files or if there were no errors
|
||||||
result["success"] = len(result["prepared_files"]) > 0 or len(result["errors"]) == 0
|
result["success"] = (
|
||||||
|
len(result["prepared_files"]) > 0 or len(result["errors"]) == 0
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(f"Auto-prepare completed for chat {chat_id}: "
|
logger.info(
|
||||||
|
f"Auto-prepare completed for chat {chat_id}: "
|
||||||
f"{len(result['prepared_files'])} prepared using {method}, "
|
f"{len(result['prepared_files'])} prepared using {method}, "
|
||||||
f"{len(result['skipped_files'])} skipped, "
|
f"{len(result['skipped_files'])} skipped, "
|
||||||
f"{len(result['errors'])} errors")
|
f"{len(result['errors'])} errors"
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -262,7 +281,9 @@ async def _test_symlink_accessibility(chat_data_dir: str, data_dir: str) -> bool
|
|||||||
logger.warning("Symlink accessibility test failed - content mismatch")
|
logger.warning("Symlink accessibility test failed - content mismatch")
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Symlink accessibility test failed - cannot read through symlink: {e}")
|
logger.warning(
|
||||||
|
f"Symlink accessibility test failed - cannot read through symlink: {e}"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Test 3: Can we stat the symlink target?
|
# Test 3: Can we stat the symlink target?
|
||||||
@ -280,9 +301,13 @@ async def _test_symlink_accessibility(chat_data_dir: str, data_dir: str) -> bool
|
|||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if "Operation not supported" in str(e) or "Function not implemented" in str(e):
|
if "Operation not supported" in str(e) or "Function not implemented" in str(e):
|
||||||
logger.info("Symlinks not supported on this filesystem - using file copying")
|
logger.info(
|
||||||
|
"Symlinks not supported on this filesystem - using file copying"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Symlink test failed with OS error: {e} - using file copying")
|
logger.warning(
|
||||||
|
f"Symlink test failed with OS error: {e} - using file copying"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Symlink test failed: {e} - using file copying")
|
logger.warning(f"Symlink test failed: {e} - using file copying")
|
||||||
@ -290,7 +315,9 @@ async def _test_symlink_accessibility(chat_data_dir: str, data_dir: str) -> bool
|
|||||||
finally:
|
finally:
|
||||||
# Clean up test files
|
# Clean up test files
|
||||||
try:
|
try:
|
||||||
if test_symlink and (os.path.exists(test_symlink) or os.path.islink(test_symlink)):
|
if test_symlink and (
|
||||||
|
os.path.exists(test_symlink) or os.path.islink(test_symlink)
|
||||||
|
):
|
||||||
os.unlink(test_symlink)
|
os.unlink(test_symlink)
|
||||||
if test_source and os.path.exists(test_source):
|
if test_source and os.path.exists(test_source):
|
||||||
os.remove(test_source)
|
os.remove(test_source)
|
||||||
@ -300,7 +327,9 @@ async def _test_symlink_accessibility(chat_data_dir: str, data_dir: str) -> bool
|
|||||||
logger.debug(f"Test cleanup failed (non-critical): {e}")
|
logger.debug(f"Test cleanup failed (non-critical): {e}")
|
||||||
|
|
||||||
|
|
||||||
async def prepare_multiple_chats_files(chat_ids: List[str], data_dir: str = "data") -> Dict[str, Any]:
|
async def prepare_multiple_chats_files(
|
||||||
|
chat_ids: List[str], data_dir: str = "data"
|
||||||
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Prepare files for multiple chats at once (bulk operation).
|
Prepare files for multiple chats at once (bulk operation).
|
||||||
|
|
||||||
@ -322,8 +351,8 @@ async def prepare_multiple_chats_files(chat_ids: List[str], data_dir: str = "dat
|
|||||||
"summary": {
|
"summary": {
|
||||||
"total_prepared_files": 0,
|
"total_prepared_files": 0,
|
||||||
"total_skipped_files": 0,
|
"total_skipped_files": 0,
|
||||||
"total_errors": 0
|
"total_errors": 0,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for chat_id in chat_ids:
|
for chat_id in chat_ids:
|
||||||
@ -338,8 +367,12 @@ async def prepare_multiple_chats_files(chat_ids: List[str], data_dir: str = "dat
|
|||||||
overall_result["success"] = False
|
overall_result["success"] = False
|
||||||
|
|
||||||
# Update summary
|
# Update summary
|
||||||
overall_result["summary"]["total_prepared_files"] += len(chat_result["prepared_files"])
|
overall_result["summary"]["total_prepared_files"] += len(
|
||||||
overall_result["summary"]["total_skipped_files"] += len(chat_result["skipped_files"])
|
chat_result["prepared_files"]
|
||||||
|
)
|
||||||
|
overall_result["summary"]["total_skipped_files"] += len(
|
||||||
|
chat_result["skipped_files"]
|
||||||
|
)
|
||||||
overall_result["summary"]["total_errors"] += len(chat_result["errors"])
|
overall_result["summary"]["total_errors"] += len(chat_result["errors"])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -347,12 +380,14 @@ async def prepare_multiple_chats_files(chat_ids: List[str], data_dir: str = "dat
|
|||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
overall_result["chat_results"][chat_id] = {
|
overall_result["chat_results"][chat_id] = {
|
||||||
"success": False,
|
"success": False,
|
||||||
"errors": [error_msg]
|
"errors": [error_msg],
|
||||||
}
|
}
|
||||||
overall_result["failed_chats"] += 1
|
overall_result["failed_chats"] += 1
|
||||||
overall_result["success"] = False
|
overall_result["success"] = False
|
||||||
|
|
||||||
logger.info(f"Bulk prepare completed: {overall_result['successful_chats']}/{overall_result['total_chats']} successful")
|
logger.info(
|
||||||
|
f"Bulk prepare completed: {overall_result['successful_chats']}/{overall_result['total_chats']} successful"
|
||||||
|
)
|
||||||
return overall_result
|
return overall_result
|
||||||
|
|
||||||
|
|
||||||
@ -369,12 +404,7 @@ def test_filesystem_support(data_dir: str = "data") -> Dict[str, Any]:
|
|||||||
"""
|
"""
|
||||||
logger.info(f"Testing filesystem support in {data_dir}")
|
logger.info(f"Testing filesystem support in {data_dir}")
|
||||||
|
|
||||||
test_result = {
|
test_result = {"success": True, "tests": {}, "errors": [], "recommendations": []}
|
||||||
"success": True,
|
|
||||||
"tests": {},
|
|
||||||
"errors": [],
|
|
||||||
"recommendations": []
|
|
||||||
}
|
|
||||||
|
|
||||||
test_dir = os.path.join(data_dir, "test_auto_prepare")
|
test_dir = os.path.join(data_dir, "test_auto_prepare")
|
||||||
|
|
||||||
@ -412,7 +442,9 @@ def test_filesystem_support(data_dir: str = "data") -> Dict[str, Any]:
|
|||||||
logger.debug("✓ Symlink creation test passed")
|
logger.debug("✓ Symlink creation test passed")
|
||||||
else:
|
else:
|
||||||
test_result["tests"]["symlink_creation"] = False
|
test_result["tests"]["symlink_creation"] = False
|
||||||
test_result["errors"].append("Cannot test symlink: source file doesn't exist")
|
test_result["errors"].append(
|
||||||
|
"Cannot test symlink: source file doesn't exist"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
test_result["tests"]["symlink_creation"] = False
|
test_result["tests"]["symlink_creation"] = False
|
||||||
test_result["errors"].append(f"Symlink creation failed: {str(e)}")
|
test_result["errors"].append(f"Symlink creation failed: {str(e)}")
|
||||||
@ -434,7 +466,9 @@ def test_filesystem_support(data_dir: str = "data") -> Dict[str, Any]:
|
|||||||
test_result["errors"].append("Symlink path resolution incorrect")
|
test_result["errors"].append("Symlink path resolution incorrect")
|
||||||
else:
|
else:
|
||||||
test_result["tests"]["path_resolution"] = False
|
test_result["tests"]["path_resolution"] = False
|
||||||
test_result["errors"].append("Cannot test path resolution: symlink doesn't exist")
|
test_result["errors"].append(
|
||||||
|
"Cannot test path resolution: symlink doesn't exist"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
test_result["tests"]["path_resolution"] = False
|
test_result["tests"]["path_resolution"] = False
|
||||||
test_result["errors"].append(f"Path resolution test failed: {str(e)}")
|
test_result["errors"].append(f"Path resolution test failed: {str(e)}")
|
||||||
@ -450,7 +484,9 @@ def test_filesystem_support(data_dir: str = "data") -> Dict[str, Any]:
|
|||||||
logger.debug("✓ Symlink accessibility test passed")
|
logger.debug("✓ Symlink accessibility test passed")
|
||||||
else:
|
else:
|
||||||
test_result["tests"]["symlink_accessibility"] = False
|
test_result["tests"]["symlink_accessibility"] = False
|
||||||
test_result["errors"].append("Symlink content mismatch - possible Docker volume issue")
|
test_result["errors"].append(
|
||||||
|
"Symlink content mismatch - possible Docker volume issue"
|
||||||
|
)
|
||||||
test_result["recommendations"].append(
|
test_result["recommendations"].append(
|
||||||
"Symlinks may not work in Docker environment. Auto-prepare will use file copying."
|
"Symlinks may not work in Docker environment. Auto-prepare will use file copying."
|
||||||
)
|
)
|
||||||
@ -485,7 +521,9 @@ def test_filesystem_support(data_dir: str = "data") -> Dict[str, Any]:
|
|||||||
if test_result["success"]:
|
if test_result["success"]:
|
||||||
logger.info("✓ All filesystem tests passed")
|
logger.info("✓ All filesystem tests passed")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠ Some filesystem tests failed: {len(test_result['errors'])} errors")
|
logger.warning(
|
||||||
|
f"⚠ Some filesystem tests failed: {len(test_result['errors'])} errors"
|
||||||
|
)
|
||||||
|
|
||||||
return test_result
|
return test_result
|
||||||
|
|
||||||
@ -720,14 +758,18 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
# Auto-prepare files for this chat before code execution
|
# Auto-prepare files for this chat before code execution
|
||||||
self.prepare_result = None
|
self.prepare_result = None
|
||||||
if self.chat_id:
|
if self.chat_id:
|
||||||
logger.info(f"Auto-preparing files for chat {self.chat_id} before code execution")
|
logger.info(
|
||||||
|
f"Auto-preparing files for chat {self.chat_id} before code execution"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
# Note: This is synchronous but auto_prepare_chat_files is async
|
# Note: This is synchronous but auto_prepare_chat_files is async
|
||||||
# We'll need to handle this in the run() method instead
|
# We'll need to handle this in the run() method instead
|
||||||
self._auto_prepare_needed = True
|
self._auto_prepare_needed = True
|
||||||
logger.debug(f"Marked auto-prepare as needed for chat {self.chat_id}")
|
logger.debug(f"Marked auto-prepare as needed for chat {self.chat_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to mark auto-prepare for chat {self.chat_id}: {str(e)}")
|
logger.error(
|
||||||
|
f"Failed to mark auto-prepare for chat {self.chat_id}: {str(e)}"
|
||||||
|
)
|
||||||
self._auto_prepare_needed = False
|
self._auto_prepare_needed = False
|
||||||
else:
|
else:
|
||||||
self._auto_prepare_needed = False
|
self._auto_prepare_needed = False
|
||||||
@ -735,7 +777,9 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
if self.base_url[-1] != "/":
|
if self.base_url[-1] != "/":
|
||||||
self.base_url += "/"
|
self.base_url += "/"
|
||||||
|
|
||||||
logger.info(f"Initializing Enterprise Gateway connection to {self.base_url} with kernel {self.kernel_name}")
|
logger.info(
|
||||||
|
f"Initializing Enterprise Gateway connection to {self.base_url} with kernel {self.kernel_name}"
|
||||||
|
)
|
||||||
if self.chat_id:
|
if self.chat_id:
|
||||||
logger.info(f"Using chat ID {self.chat_id} for path replacement")
|
logger.info(f"Using chat ID {self.chat_id} for path replacement")
|
||||||
self.session = aiohttp.ClientSession(trust_env=True, base_url=self.base_url)
|
self.session = aiohttp.ClientSession(trust_env=True, base_url=self.base_url)
|
||||||
@ -748,17 +792,25 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.prepare_result = await auto_prepare_chat_files(self.chat_id, self.data_dir)
|
self.prepare_result = await auto_prepare_chat_files(
|
||||||
|
self.chat_id, self.data_dir
|
||||||
|
)
|
||||||
if self.prepare_result["success"]:
|
if self.prepare_result["success"]:
|
||||||
prepared_count = len(self.prepare_result["prepared_files"])
|
prepared_count = len(self.prepare_result["prepared_files"])
|
||||||
if prepared_count > 0:
|
if prepared_count > 0:
|
||||||
logger.info(f"Successfully prepared {prepared_count} files for chat {self.chat_id}")
|
logger.info(
|
||||||
|
f"Successfully prepared {prepared_count} files for chat {self.chat_id}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"No files to prepare for chat {self.chat_id}")
|
logger.debug(f"No files to prepare for chat {self.chat_id}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"File preparation had issues for chat {self.chat_id}: {self.prepare_result['errors']}")
|
logger.warning(
|
||||||
|
f"File preparation had issues for chat {self.chat_id}: {self.prepare_result['errors']}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to auto-prepare files for chat {self.chat_id}: {str(e)}")
|
logger.error(
|
||||||
|
f"Failed to auto-prepare files for chat {self.chat_id}: {str(e)}"
|
||||||
|
)
|
||||||
# Continue with execution even if file preparation fails
|
# Continue with execution even if file preparation fails
|
||||||
|
|
||||||
def _prepare_code_with_path_replacement(self, code: str) -> str:
|
def _prepare_code_with_path_replacement(self, code: str) -> str:
|
||||||
@ -812,7 +864,9 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
if self.kernel_id:
|
if self.kernel_id:
|
||||||
try:
|
try:
|
||||||
async with self.session.delete(f"api/kernels/{self.kernel_id}", headers=self.headers) as response:
|
async with self.session.delete(
|
||||||
|
f"api/kernels/{self.kernel_id}", headers=self.headers
|
||||||
|
) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
logger.info(f"Closed kernel {self.kernel_id}")
|
logger.info(f"Closed kernel {self.kernel_id}")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
@ -843,7 +897,7 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
"env": {
|
"env": {
|
||||||
"KERNEL_USERNAME": self.username,
|
"KERNEL_USERNAME": self.username,
|
||||||
"KERNEL_ID": str(uuid.uuid4()),
|
"KERNEL_ID": str(uuid.uuid4()),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"Starting {self.kernel_name} kernel for user {self.username}")
|
logger.info(f"Starting {self.kernel_name} kernel for user {self.username}")
|
||||||
@ -870,7 +924,9 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
async def execute_code(self) -> None:
|
async def execute_code(self) -> None:
|
||||||
websocket_url, headers = self.init_ws()
|
websocket_url, headers = self.init_ws()
|
||||||
try:
|
try:
|
||||||
async with websockets.connect(websocket_url, additional_headers=headers) as ws:
|
async with websockets.connect(
|
||||||
|
websocket_url, additional_headers=headers
|
||||||
|
) as ws:
|
||||||
await self.execute_in_gateway(ws)
|
await self.execute_in_gateway(ws)
|
||||||
except websockets.exceptions.WebSocketException as e:
|
except websockets.exceptions.WebSocketException as e:
|
||||||
logger.error(f"WebSocket error: {e}")
|
logger.error(f"WebSocket error: {e}")
|
||||||
@ -893,7 +949,7 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
"msg_type": "execute_request",
|
"msg_type": "execute_request",
|
||||||
"username": self.username,
|
"username": self.username,
|
||||||
"session": str(uuid.uuid4()),
|
"session": str(uuid.uuid4()),
|
||||||
"version": "5.4"
|
"version": "5.4",
|
||||||
},
|
},
|
||||||
"parent_header": {},
|
"parent_header": {},
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
@ -903,10 +959,10 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
"store_history": True,
|
"store_history": True,
|
||||||
"user_expressions": {},
|
"user_expressions": {},
|
||||||
"allow_stdin": False,
|
"allow_stdin": False,
|
||||||
"stop_on_error": True
|
"stop_on_error": True,
|
||||||
},
|
},
|
||||||
"buffers": [],
|
"buffers": [],
|
||||||
"channel": "shell"
|
"channel": "shell",
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(f"Sending execute request with msg_id {msg_id}")
|
logger.debug(f"Sending execute request with msg_id {msg_id}")
|
||||||
@ -947,7 +1003,9 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
results.append(result_text)
|
results.append(result_text)
|
||||||
logger.debug(f"Result text: {result_text}")
|
logger.debug(f"Result text: {result_text}")
|
||||||
if "image/png" in response["content"]["data"]:
|
if "image/png" in response["content"]["data"]:
|
||||||
results.append(f"data:image/png;base64,{response['content']['data']['image/png']}")
|
results.append(
|
||||||
|
f"data:image/png;base64,{response['content']['data']['image/png']}"
|
||||||
|
)
|
||||||
logger.debug("Added image result")
|
logger.debug("Added image result")
|
||||||
|
|
||||||
elif msg_type == "display_data":
|
elif msg_type == "display_data":
|
||||||
@ -958,29 +1016,35 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
results.append(result_text)
|
results.append(result_text)
|
||||||
logger.debug(f"Display text: {result_text}")
|
logger.debug(f"Display text: {result_text}")
|
||||||
if "image/png" in response["content"]["data"]:
|
if "image/png" in response["content"]["data"]:
|
||||||
results.append(f"data:image/png;base64,{response['content']['data']['image/png']}")
|
results.append(
|
||||||
|
f"data:image/png;base64,{response['content']['data']['image/png']}"
|
||||||
|
)
|
||||||
logger.debug("Added image display")
|
logger.debug("Added image display")
|
||||||
|
|
||||||
elif msg_type == "error":
|
elif msg_type == "error":
|
||||||
error = {
|
error = {
|
||||||
"ename": response["content"]["ename"],
|
"ename": response["content"]["ename"],
|
||||||
"evalue": response["content"]["evalue"],
|
"evalue": response["content"]["evalue"],
|
||||||
"traceback": response["content"]["traceback"]
|
"traceback": response["content"]["traceback"],
|
||||||
}
|
}
|
||||||
stderr_content += "\n".join(error["traceback"])
|
stderr_content += "\n".join(error["traceback"])
|
||||||
logger.debug(f"Execution error: {error}")
|
logger.debug(f"Execution error: {error}")
|
||||||
|
|
||||||
elif msg_type == "execute_reply":
|
elif msg_type == "execute_reply":
|
||||||
logger.debug(f"Execute reply status: {response['content']['status']}")
|
logger.debug(
|
||||||
|
f"Execute reply status: {response['content']['status']}"
|
||||||
|
)
|
||||||
if response["content"]["status"] == "ok":
|
if response["content"]["status"] == "ok":
|
||||||
logger.debug("Received execute_reply with status=ok")
|
logger.debug("Received execute_reply with status=ok")
|
||||||
break
|
break
|
||||||
elif response["content"]["status"] == "error":
|
elif response["content"]["status"] == "error":
|
||||||
if not error: # Only add if we haven't already processed an error message
|
if (
|
||||||
|
not error
|
||||||
|
): # Only add if we haven't already processed an error message
|
||||||
error = {
|
error = {
|
||||||
"ename": response["content"]["ename"],
|
"ename": response["content"]["ename"],
|
||||||
"evalue": response["content"]["evalue"],
|
"evalue": response["content"]["evalue"],
|
||||||
"traceback": response["content"]["traceback"]
|
"traceback": response["content"]["traceback"],
|
||||||
}
|
}
|
||||||
stderr_content += "\n".join(error["traceback"])
|
stderr_content += "\n".join(error["traceback"])
|
||||||
logger.debug("Received execute_reply with status=error")
|
logger.debug("Received execute_reply with status=error")
|
||||||
@ -996,9 +1060,15 @@ class EnterpriseGatewayCodeExecutor:
|
|||||||
logger.warning(f"Execution timed out after {self.timeout}s")
|
logger.warning(f"Execution timed out after {self.timeout}s")
|
||||||
break
|
break
|
||||||
|
|
||||||
self.result.stdout = self._prepare_results_with_path_replacement(stdout_content.strip())
|
self.result.stdout = self._prepare_results_with_path_replacement(
|
||||||
self.result.stderr = self._prepare_results_with_path_replacement(stderr_content.strip())
|
stdout_content.strip()
|
||||||
self.result.result = self._prepare_results_with_path_replacement("\n".join(results).strip() if results else "")
|
)
|
||||||
|
self.result.stderr = self._prepare_results_with_path_replacement(
|
||||||
|
stderr_content.strip()
|
||||||
|
)
|
||||||
|
self.result.result = self._prepare_results_with_path_replacement(
|
||||||
|
"\n".join(results).strip() if results else ""
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(f"Final result - stdout: {self.result.stdout}")
|
logger.debug(f"Final result - stdout: {self.result.stdout}")
|
||||||
logger.debug(f"Final result - stderr: {self.result.stderr}")
|
logger.debug(f"Final result - stderr: {self.result.stderr}")
|
||||||
@ -1015,6 +1085,7 @@ async def deprecated_execute_code_jupyter(
|
|||||||
result = await executor.run()
|
result = await executor.run()
|
||||||
return result.model_dump()
|
return result.model_dump()
|
||||||
|
|
||||||
|
|
||||||
async def execute_code_jupyter(
|
async def execute_code_jupyter(
|
||||||
base_url: str,
|
base_url: str,
|
||||||
code: str,
|
code: str,
|
||||||
@ -1022,7 +1093,7 @@ async def execute_code_jupyter(
|
|||||||
password: str = "",
|
password: str = "",
|
||||||
timeout: int = 60,
|
timeout: int = 60,
|
||||||
chat_id: str = "",
|
chat_id: str = "",
|
||||||
data_dir: str = "data"
|
data_dir: str = "data",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
async with EnterpriseGatewayCodeExecutor(
|
async with EnterpriseGatewayCodeExecutor(
|
||||||
base_url, code, token, password, timeout, chat_id=chat_id, data_dir=data_dir
|
base_url, code, token, password, timeout, chat_id=chat_id, data_dir=data_dir
|
||||||
@ -1034,7 +1105,7 @@ async def execute_code_jupyter(
|
|||||||
def generate_dynamic_code_interpreter_prompt(
|
def generate_dynamic_code_interpreter_prompt(
|
||||||
base_prompt: str,
|
base_prompt: str,
|
||||||
chat_id: str = "",
|
chat_id: str = "",
|
||||||
attached_files: Optional[List[Dict[str, Any]]] = None
|
attached_files: Optional[List[Dict[str, Any]]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate a dynamic code interpreter prompt that includes information about attached files.
|
Generate a dynamic code interpreter prompt that includes information about attached files.
|
||||||
@ -1059,7 +1130,9 @@ def generate_dynamic_code_interpreter_prompt(
|
|||||||
# Create file information section
|
# Create file information section
|
||||||
file_info_lines = []
|
file_info_lines = []
|
||||||
file_info_lines.append("\n#### Available Files")
|
file_info_lines.append("\n#### Available Files")
|
||||||
file_info_lines.append("The following files have been attached to this conversation and are available in `/mnt/data/`:")
|
file_info_lines.append(
|
||||||
|
"The following files have been attached to this conversation and are available in `/mnt/data/`:"
|
||||||
|
)
|
||||||
file_info_lines.append("")
|
file_info_lines.append("")
|
||||||
|
|
||||||
for file_info in attached_files:
|
for file_info in attached_files:
|
||||||
@ -1077,30 +1150,48 @@ def generate_dynamic_code_interpreter_prompt(
|
|||||||
else:
|
else:
|
||||||
size_str = f" ({file_size / (1024 * 1024):.1f} MB)"
|
size_str = f" ({file_size / (1024 * 1024):.1f} MB)"
|
||||||
|
|
||||||
file_info_lines.append(f"- **{file_name}**{size_str} - Available at `/mnt/data/{file_name}`")
|
file_info_lines.append(
|
||||||
|
f"- **{file_name}**{size_str} - Available at `/mnt/data/{file_name}`"
|
||||||
|
)
|
||||||
|
|
||||||
# Add file type specific suggestions
|
# Add file type specific suggestions
|
||||||
if file_name.lower().endswith(('.csv', '.tsv')):
|
if file_name.lower().endswith((".csv", ".tsv")):
|
||||||
file_info_lines.append(f" - Data file - Use `pd.read_csv('/mnt/data/{file_name}')` to load")
|
file_info_lines.append(
|
||||||
elif file_name.lower().endswith(('.xlsx', '.xls')):
|
f" - Data file - Use `pd.read_csv('/mnt/data/{file_name}')` to load"
|
||||||
file_info_lines.append(f" - Excel file - Use `pd.read_excel('/mnt/data/{file_name}')` to load")
|
)
|
||||||
elif file_name.lower().endswith(('.json', '.jsonl')):
|
elif file_name.lower().endswith((".xlsx", ".xls")):
|
||||||
file_info_lines.append(f" - JSON file - Use `pd.read_json('/mnt/data/{file_name}')` or `json.load()` to load")
|
file_info_lines.append(
|
||||||
elif file_name.lower().endswith(('.txt', '.md', '.py', '.js', '.html', '.css')):
|
f" - Excel file - Use `pd.read_excel('/mnt/data/{file_name}')` to load"
|
||||||
file_info_lines.append(f" - Text file - Use `open('/mnt/data/{file_name}', 'r').read()` to load")
|
)
|
||||||
elif file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff')):
|
elif file_name.lower().endswith((".json", ".jsonl")):
|
||||||
file_info_lines.append(f" - Image file - Use `PIL.Image.open('/mnt/data/{file_name}')` or `cv2.imread()` to load")
|
file_info_lines.append(
|
||||||
elif file_name.lower().endswith(('.pdf')):
|
f" - JSON file - Use `pd.read_json('/mnt/data/{file_name}')` or `json.load()` to load"
|
||||||
file_info_lines.append(f" - PDF file - Use `PyPDF2` or `pdfplumber` to extract text/data")
|
)
|
||||||
|
elif file_name.lower().endswith((".txt", ".md", ".py", ".js", ".html", ".css")):
|
||||||
|
file_info_lines.append(
|
||||||
|
f" - Text file - Use `open('/mnt/data/{file_name}', 'r').read()` to load"
|
||||||
|
)
|
||||||
|
elif file_name.lower().endswith(
|
||||||
|
(".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff")
|
||||||
|
):
|
||||||
|
file_info_lines.append(
|
||||||
|
f" - Image file - Use `PIL.Image.open('/mnt/data/{file_name}')` or `cv2.imread()` to load"
|
||||||
|
)
|
||||||
|
elif file_name.lower().endswith((".pdf")):
|
||||||
|
file_info_lines.append(
|
||||||
|
f" - PDF file - Use `PyPDF2` or `pdfplumber` to extract text/data"
|
||||||
|
)
|
||||||
|
|
||||||
file_info_lines.append("")
|
file_info_lines.append("")
|
||||||
file_info_lines.append("**Important**: These files are immediately ready to use - no upload needed. Reference them directly by their paths above.")
|
file_info_lines.append(
|
||||||
|
"**Important**: These files are immediately ready to use - no upload needed. Reference them directly by their paths above."
|
||||||
|
)
|
||||||
|
|
||||||
# Insert file information after the main code interpreter description but before the final note
|
# Insert file information after the main code interpreter description but before the final note
|
||||||
file_info_section = "\n".join(file_info_lines)
|
file_info_section = "\n".join(file_info_lines)
|
||||||
|
|
||||||
# Find a good insertion point in the base prompt
|
# Find a good insertion point in the base prompt
|
||||||
prompt_lines = base_prompt.split('\n')
|
prompt_lines = base_prompt.split("\n")
|
||||||
|
|
||||||
# Look for the line about /mnt/data and insert file info after it
|
# Look for the line about /mnt/data and insert file info after it
|
||||||
insertion_point = -1
|
insertion_point = -1
|
||||||
@ -1112,11 +1203,11 @@ def generate_dynamic_code_interpreter_prompt(
|
|||||||
if insertion_point > 0:
|
if insertion_point > 0:
|
||||||
# Insert file information after the /mnt/data line
|
# Insert file information after the /mnt/data line
|
||||||
enhanced_lines = (
|
enhanced_lines = (
|
||||||
prompt_lines[:insertion_point] +
|
prompt_lines[:insertion_point]
|
||||||
file_info_section.split('\n') +
|
+ file_info_section.split("\n")
|
||||||
prompt_lines[insertion_point:]
|
+ prompt_lines[insertion_point:]
|
||||||
)
|
)
|
||||||
return '\n'.join(enhanced_lines)
|
return "\n".join(enhanced_lines)
|
||||||
else:
|
else:
|
||||||
# Fallback: append file information at the end
|
# Fallback: append file information at the end
|
||||||
return base_prompt + "\n" + file_info_section
|
return base_prompt + "\n" + file_info_section
|
@ -81,7 +81,10 @@ from open_webui.utils.filter import (
|
|||||||
get_sorted_filter_ids,
|
get_sorted_filter_ids,
|
||||||
process_filter_functions,
|
process_filter_functions,
|
||||||
)
|
)
|
||||||
from open_webui.utils.code_interpreter import execute_code_jupyter, generate_dynamic_code_interpreter_prompt
|
from open_webui.utils.code_interpreter import (
|
||||||
|
execute_code_jupyter,
|
||||||
|
generate_dynamic_code_interpreter_prompt,
|
||||||
|
)
|
||||||
|
|
||||||
from open_webui.tasks import create_task
|
from open_webui.tasks import create_task
|
||||||
|
|
||||||
@ -854,7 +857,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||||||
enhanced_prompt = generate_dynamic_code_interpreter_prompt(
|
enhanced_prompt = generate_dynamic_code_interpreter_prompt(
|
||||||
base_prompt=base_prompt,
|
base_prompt=base_prompt,
|
||||||
attached_files=attached_files,
|
attached_files=attached_files,
|
||||||
chat_id=metadata.get("chat_id", "")
|
chat_id=metadata.get("chat_id", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
form_data["messages"] = add_or_update_user_message(
|
form_data["messages"] = add_or_update_user_message(
|
||||||
@ -2278,7 +2281,7 @@ async def process_chat_response(
|
|||||||
),
|
),
|
||||||
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
|
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
|
||||||
chat_id=metadata.get("chat_id", ""),
|
chat_id=metadata.get("chat_id", ""),
|
||||||
data_dir="data"
|
data_dir="data",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
output = {
|
output = {
|
||||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.15",
|
"version": "0.6.15c",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.15",
|
"version": "0.6.15c",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^4.5.0",
|
"@azure/msal-browser": "^4.5.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.15",
|
"version": "0.6.15c",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
|
Loading…
Reference in New Issue
Block a user