Revised all files to reduce bloat + optimized workflow
This commit is contained in:
parent
af1ca55611
commit
dcc04ca6ca
111 changed files with 17862 additions and 0 deletions
38
.dockerignore
Normal file
38
.dockerignore
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
*.sqlite3
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.db
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.mo
|
||||||
|
*.swp
|
||||||
|
*.yml
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.git/
|
||||||
|
.github/
|
||||||
|
.gitignore
|
||||||
|
.editorconfig
|
||||||
|
.prettierrc
|
||||||
|
.prettierignore
|
||||||
|
.nx/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv/
|
||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
celerybeat-schedule
|
||||||
|
*.md
|
||||||
|
*.bat
|
||||||
|
notebooks/
|
||||||
|
documents/
|
||||||
|
models/
|
||||||
|
eslint.config.mjs
|
||||||
26
.editorconfig
Normal file
26
.editorconfig
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[compose/**.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[compose/**.yaml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[docker-compose*.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
||||||
33
.env.example
Normal file
33
.env.example
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Compose
|
||||||
|
COMPOSE_PROJECT_NAME=dynavera
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
DJANGO_FRONT_DIR=site/build
|
||||||
|
DJANGO_MODEL_DIR=model
|
||||||
|
|
||||||
|
# Django core
|
||||||
|
DJANGO_SECRET_KEY=pe2=12*opi$3+=j3e6e!&*!7bfd9dsg!km*e4q77am%*_fzou(
|
||||||
|
DJANGO_DEBUG=True
|
||||||
|
DJANGO_DOMAIN_NAME=localhost
|
||||||
|
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
DJANGO_CELERY_BROKER_URL=redis://fyp-redis-dev:6379/0
|
||||||
|
|
||||||
|
# Static & Media paths
|
||||||
|
DJANGO_STATIC_URL=/static/
|
||||||
|
DJANGO_MEDIA_URL=/media/
|
||||||
|
DJANGO_STATIC_ROOT=static
|
||||||
|
DJANGO_MEDIA_ROOT=media
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DJANGO_DB_ENGINE=django.db.backends.postgresql_psycopg2
|
||||||
|
POSTGRES_DB=FinalYearProject
|
||||||
|
POSTGRES_USER=jouUfa9C21%A
|
||||||
|
POSTGRES_PASSWORD=a7E/5sUH@03v
|
||||||
|
POSTGRES_HOST=fyp-postgres-dev
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
|
# Inference server
|
||||||
|
INFERENCE_HOST=fyp-inference-dev
|
||||||
|
INFERENCE_PORT=8001
|
||||||
48
.env.template
Normal file
48
.env.template
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Django .env template file
|
||||||
|
|
||||||
|
# Compose
|
||||||
|
COMPOSE_PROJECT_NAME=dynavera
|
||||||
|
|
||||||
|
# Directories
|
||||||
|
DJANGO_FRONT_DIR=front
|
||||||
|
DJANGO_MODEL_DIR=model
|
||||||
|
|
||||||
|
# Django core
|
||||||
|
DJANGO_SECRET_KEY=change_this_to_a_secure_key
|
||||||
|
DJANGO_DEBUG=False
|
||||||
|
DJANGO_DOMAIN_NAME=localhost
|
||||||
|
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
DJANGO_CELERY_BROKER_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
|
# Static & Media paths
|
||||||
|
DJANGO_STATIC_URL=/static/
|
||||||
|
DJANGO_MEDIA_URL=/media/
|
||||||
|
DJANGO_STATIC_ROOT=static
|
||||||
|
DJANGO_MEDIA_ROOT=media
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DJANGO_DB_ENGINE=django.db.backends.sqlite3
|
||||||
|
POSTGRES_DB=postgres_db_name
|
||||||
|
POSTGRES_USER=postgres_user
|
||||||
|
POSTGRES_PASSWORD=postgres_password
|
||||||
|
POSTGRES_HOST=localhost
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
|
# Inference server
|
||||||
|
INFERENCE_HOST=localhost
|
||||||
|
INFERENCE_PORT=8001
|
||||||
|
|
||||||
|
# Production YAML (Ignore if you're setting up locally)
|
||||||
|
FYP_DJANGO_IMAGE=dynavera-django:prod
|
||||||
|
FYP_CELERY_IMAGE=dynavera-celery:prod
|
||||||
|
DJANGO_ENTRYPOINT=websecure
|
||||||
|
CERTRESOLVER=myresolver
|
||||||
|
DJANGO_PORT=8000
|
||||||
|
GITLAB_USER=yourgitlabuser
|
||||||
|
GITLAB_PASS=yourgitlabpass
|
||||||
|
GITLAB_SERVER_URL=https://gitlab.com/
|
||||||
|
GITLAB_RUNNER_REGISTRATION_TOKEN=your_registration_token
|
||||||
|
GITLAB_RUNNER_DOCKER_IMAGE=python:3.10-slim
|
||||||
|
GITLAB_RUNNER_IMAGE_TAG=latest
|
||||||
269
.gitignore
vendored
Normal file
269
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[codz]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
# Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
# poetry.lock
|
||||||
|
# poetry.toml
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||||
|
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||||
|
# pdm.lock
|
||||||
|
# pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# pixi
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||||
|
# pixi.lock
|
||||||
|
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||||
|
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||||
|
.pixi
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
*.rdb
|
||||||
|
*.aof
|
||||||
|
*.pid
|
||||||
|
|
||||||
|
# RabbitMQ
|
||||||
|
mnesia/
|
||||||
|
rabbitmq/
|
||||||
|
rabbitmq-data/
|
||||||
|
|
||||||
|
# ActiveMQ
|
||||||
|
activemq-data/
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
.envrc
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
# .idea/
|
||||||
|
|
||||||
|
# Abstra
|
||||||
|
# Abstra is an AI-powered process automation framework.
|
||||||
|
# Ignore directories containing user credentials, local state, and settings.
|
||||||
|
# Learn more at https://abstra.io/docs
|
||||||
|
.abstra/
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
|
|
||||||
|
# Streamlit
|
||||||
|
.streamlit/secrets.toml
|
||||||
|
|
||||||
|
# Build
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Vitest
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
|
# Local batch files
|
||||||
|
*.local.bat
|
||||||
|
|
||||||
|
# Static files
|
||||||
|
static/
|
||||||
|
|
||||||
|
# Media files
|
||||||
|
media/
|
||||||
|
|
||||||
|
# Models
|
||||||
|
model/
|
||||||
|
|
||||||
|
# Gihub files
|
||||||
|
.github/
|
||||||
57
.gitlab-ci.yml
Normal file
57
.gitlab-ci.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- lint
|
||||||
|
- build
|
||||||
|
|
||||||
|
run_tests:
|
||||||
|
stage: test
|
||||||
|
image: python:3.12
|
||||||
|
variables:
|
||||||
|
DJANGO_SECRET_KEY: 'random_secret_key_for_ci'
|
||||||
|
before_script:
|
||||||
|
- python -m pip install --upgrade pip
|
||||||
|
- pip install --no-cache-dir -r requirements/django.txt
|
||||||
|
script:
|
||||||
|
- python manage.py test --verbosity=2
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main"
|
||||||
|
|
||||||
|
check_node_syntax:
|
||||||
|
stage: lint
|
||||||
|
image: node:20-alpine
|
||||||
|
before_script:
|
||||||
|
- npm ci
|
||||||
|
script:
|
||||||
|
- npm run type-check
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH == "main"
|
||||||
|
|
||||||
|
build_and_push:
|
||||||
|
stage: build
|
||||||
|
image: docker:24.0.7
|
||||||
|
variables:
|
||||||
|
DOCKER_HOST: tcp://docker:2375
|
||||||
|
DOCKER_TLS_CERTDIR: ''
|
||||||
|
services:
|
||||||
|
- name: docker:24.0.7-dind
|
||||||
|
alias: docker
|
||||||
|
command: ['--tls=false', '--host=tcp://0.0.0.0:2375']
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache git
|
||||||
|
script:
|
||||||
|
- echo "Waiting for Docker daemon..."
|
||||||
|
- for i in $(seq 1 30); do docker info && break || sleep 1; done
|
||||||
|
- echo "Logging in to registry ${REGISTRY_URL}"
|
||||||
|
- echo "$REGISTRY_PASSWORD" | docker login -u "$REGISTRY_USERNAME" --password-stdin "$REGISTRY_URL"
|
||||||
|
- export DJANGO_IMAGE_NAME="${REGISTRY_URL}/${DJANGO_IMAGE_PATH}:${IMAGE_TAG}"
|
||||||
|
- echo "Building image ${DJANGO_IMAGE_NAME}"
|
||||||
|
- docker build -t "$DJANGO_IMAGE_NAME" -f ./compose/prod/django/Dockerfile --no-cache .
|
||||||
|
- echo "Pushing image ${DJANGO_IMAGE_NAME}"
|
||||||
|
- docker push "$DJANGO_IMAGE_NAME"
|
||||||
|
- export CELERY_IMAGE_NAME="${REGISTRY_URL}/${CELERY_IMAGE_PATH}:${IMAGE_TAG}"
|
||||||
|
- echo "Building Celery image ${CELERY_IMAGE_NAME}"
|
||||||
|
- docker build -t "$CELERY_IMAGE_NAME" -f ./compose/prod/celery/Dockerfile --no-cache .
|
||||||
|
- echo "Pushing Celery image ${CELERY_IMAGE_NAME}"
|
||||||
|
- docker push "$CELERY_IMAGE_NAME"
|
||||||
|
needs:
|
||||||
|
- run_tests
|
||||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"EditorConfig.EditorConfig"
|
||||||
|
]
|
||||||
|
}
|
||||||
103
README.md
Normal file
103
README.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# Dynavera: Distributed Agentic Onboarding System
|
||||||
|
|
||||||
|
Dynavera is a multi-agent AI platform designed to automate role-specific onboarding. The system utilizes a distributed architecture to separate application logic from high-latency LLM inference, employing the Model Context Protocol (MCP) for internal data retrieval and Retrieval-Augmented Generation (RAG).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Goals
|
||||||
|
|
||||||
|
- [x] Distributed Orchestration: Implementation of a dual-node system (VPS/GPU) to manage real-time user interaction and heavy computational inference independently.
|
||||||
|
|
||||||
|
- [x] Context-Aware Training: Development of a RAG pipeline that utilizes semantic chunking and vector similarity search to provide role-specific guidance.
|
||||||
|
|
||||||
|
- [x] Agentic Workflow: Utilizing an orchestrator to manage stateful conversations, tool calls, and user progress tracking via WebSockets.
|
||||||
|
|
||||||
|
- [x] Automated Ingestion: Creating a pipeline for converting raw organizational documents (PDF/TXT) into searchable vector embeddings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Architecture
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The application is split into two primary layers:
|
||||||
|
|
||||||
|
### Management Layer (VPS)
|
||||||
|
* **Framework**: Django 5.x with Django Channels for WebSocket management.
|
||||||
|
* **Database**: PostgreSQL with the pgvector extension for semantic storage.
|
||||||
|
* **Task Queue**: Celery and Redis for asynchronous document processing and ingestion.
|
||||||
|
* **Internal Routing**: `apps/onboarding/mcp.py` serves as the Model Context Protocol router, bridging the agent to the PostgreSQL vector store.
|
||||||
|
|
||||||
|
### Intelligence Layer (GPU Node)
|
||||||
|
* **Inference Server**: `gpu_server.py` (FastAPI) located in the root, exposing endpoints for LLM chat completions and embeddings.
|
||||||
|
* **Semantic Processor**: Custom logic within the inference server for smart chunking that detects topic shifts in text to optimize retrieval accuracy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
* **Backend**: Django, Django REST Framework, Django Channels.
|
||||||
|
* **Frontend**: Vue 3, Vite, Pinia.
|
||||||
|
* **Database**: PostgreSQL (pgvector).
|
||||||
|
* **AI/ML**: FastAPI, OpenAI-compatible API structures, Sentence-Transformers.
|
||||||
|
* **Infrastructure**: Docker, Redis, Celery.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Application Structure
|
||||||
|
|
||||||
|
* **apps.accounts**: Manages User, Organization, and Role models, including invite-based onboarding logic.
|
||||||
|
* **apps.knowledge**: Handles the RAG pipeline, including TrainingFile management and RoleRagDocument vector storage.
|
||||||
|
* **apps.onboarding**: Contains the core logic for the onboarding experience:
|
||||||
|
* `consumers.py`: The Agent Orchestrator managing WebSocket handshakes and session loops.
|
||||||
|
* `mcp.py`: The internal router for Model Context Protocol tool execution.
|
||||||
|
* `models.py`: Stores AgentConfig (prompts/tools) and OnboardingSession state.
|
||||||
|
* **gpu_server.py**: The entry point for the Intelligence Layer, handling embedding generation and LLM inference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Instructions for Evaluation
|
||||||
|
|
||||||
|
The system is currently pre-loaded with demonstration data from internal configuration files.
|
||||||
|
|
||||||
|
### Access Credentials
|
||||||
|
|
||||||
|
| Role | Email | Password |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Admin** | admin@example.com | admin |
|
||||||
|
| **Manager** | haleisaac@example.com | password |
|
||||||
|
| **User** | j.thompson@example.com | password |
|
||||||
|
|
||||||
|
### Recommended Technical Walkthrough
|
||||||
|
|
||||||
|
To verify the integration of the Knowledge Pipeline and the Agentic Orchestrator, follow these steps:
|
||||||
|
|
||||||
|
1. **Environment Setup**: Navigate to https://fyp.viswamedha.com. *
|
||||||
|
2. **Document Ingestion**: Log in as the **Manager** (haleisaac@example.com). Navigate to the **University of Birmingham** organization. Upload a PDF relevant to a specific role.
|
||||||
|
3. **Vectorization**: Observe the ingestion status. The system will extract text, send it to the GPU node for semantic chunking, and store the resulting 1536-dimension vectors in PostgreSQL.
|
||||||
|
4. **Agent Interaction**: Access the **Role Onboarding** interface. Initiate a session.
|
||||||
|
5. **Retrieval Verification**: This will query the agent regarding specific details within the uploaded PDF. The agent in `consumers.py` will trigger a tool call via `mcp.py`, retrieve the relevant document chunks, and provide a contextualized response via onboarding pages.
|
||||||
|
|
||||||
|
*Note: If the website that I hosted is not accessible, please set up the project locally by following the instructions in the Usage section below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Clone the repository.
|
||||||
|
2. Copy the `.env.example` file to `.env` or create a new `.env` file based on `.env.template`, and change the necessary environment variables. *
|
||||||
|
3. Deploy via Docker Compose: `docker compose -f compose/dev/docker-compose.yml --env-file .env up -d` in the root directory.
|
||||||
|
4. Access the frontend at the configured port (usually `localhost:8000`).
|
||||||
|
|
||||||
|
* Note: If you use a different secret key, when the fyp-django-dev container starts, you will need to execute the following command to reset all accounts to default passwords of "admin" for admin users and "password" for manager and user accounts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it fyp-django-dev python manage.py reset_passwords
|
||||||
|
```
|
||||||
|
|
||||||
|
### Warnings
|
||||||
|
|
||||||
|
* The development compose is used here to allow HMR and easier debugging. Please only use this file.
|
||||||
|
* Ensure that a GPU is available and CUDA drivers are properly installed for the inference server to function.
|
||||||
|
* I have tested this on an RTX 3060 with 12GB VRAM, so I am not sure if it will work on other GPUs.
|
||||||
|
* There is no guarantee that it will load on a CPU-only machine as the batch size and model parameters are configured for GPU inference.
|
||||||
0
apps/__init__.py
Normal file
0
apps/__init__.py
Normal file
0
apps/accounts/__init__.py
Normal file
0
apps/accounts/__init__.py
Normal file
52
apps/accounts/admin.py
Normal file
52
apps/accounts/admin.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.admin import ModelAdmin, TabularInline
|
||||||
|
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
from apps.accounts.models import User, Role, Organization, Invite
|
||||||
|
|
||||||
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
@admin.register(User)
|
||||||
|
class UserAdmin(DjangoUserAdmin):
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('email_address', 'password')}),
|
||||||
|
('Personal info', {'fields': ('first_name', 'last_name')}),
|
||||||
|
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'is_manager')}),
|
||||||
|
('Dates', {'fields': ('last_login',)}),
|
||||||
|
)
|
||||||
|
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ('email_address', 'first_name', 'last_name', 'password1', 'password2'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
list_display = ('email_address', 'first_name', 'last_name', 'is_staff', 'is_manager')
|
||||||
|
search_fields = ('email_address', 'first_name', 'last_name')
|
||||||
|
ordering = ('email_address',)
|
||||||
|
|
||||||
|
@admin.register(Organization)
|
||||||
|
class OrganizationAdmin(ModelAdmin):
|
||||||
|
list_display = ('name', 'owner', 'uuid', 'created_at')
|
||||||
|
search_fields = ('name', 'owner__email_address')
|
||||||
|
list_filter = ('created_at',)
|
||||||
|
raw_id_fields = ('owner',)
|
||||||
|
readonly_fields = ('uuid', 'created_at', 'updated_at')
|
||||||
|
|
||||||
|
@admin.register(Invite)
|
||||||
|
class InviteAdmin(ModelAdmin):
|
||||||
|
list_display = ('token', 'organization', 'created_by', 'is_active', 'uses', 'max_uses', 'expires_at')
|
||||||
|
search_fields = ('token', 'organization__name', 'created_by__email_address')
|
||||||
|
list_filter = ('is_active', 'expires_at')
|
||||||
|
raw_id_fields = ('organization', 'created_by')
|
||||||
|
readonly_fields = ('token', 'created_at')
|
||||||
|
|
||||||
|
@admin.register(Role)
|
||||||
|
class RoleAdmin(ModelAdmin):
|
||||||
|
list_display = ('name', 'organization', 'uuid')
|
||||||
|
search_fields = ('name', 'organization__name')
|
||||||
|
list_filter = ('organization',)
|
||||||
|
raw_id_fields = ('organization',)
|
||||||
|
readonly_fields = ('uuid', 'created_at', 'updated_at')
|
||||||
5
apps/accounts/apps.py
Normal file
5
apps/accounts/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.accounts'
|
||||||
14
apps/accounts/management/commands/reset_passwords.py
Normal file
14
apps/accounts/management/commands/reset_passwords.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Resets non-admin account passwords to "password" and admin account passwords to "admin" using the current SECRET_KEY'
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
for user in User.objects.all():
|
||||||
|
if user.is_staff:
|
||||||
|
user.set_password('admin')
|
||||||
|
else:
|
||||||
|
user.set_password('password')
|
||||||
|
user.save()
|
||||||
|
self.stdout.write("All account passwords synchronized with local SECRET_KEY.")
|
||||||
27
apps/accounts/managers.py
Normal file
27
apps/accounts/managers.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.contrib.auth.models import BaseUserManager
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from apps.accounts.models import User
|
||||||
|
|
||||||
|
class UserManager(BaseUserManager["User"]):
|
||||||
|
|
||||||
|
def _create_user(self, email_address: str, password: str | None, **extra_fields):
|
||||||
|
if not email_address:
|
||||||
|
raise ValueError("The given email must be set")
|
||||||
|
email_address = self.normalize_email(email_address)
|
||||||
|
user: User = self.model(email_address=email_address, **extra_fields)
|
||||||
|
user.password = make_password(password)
|
||||||
|
user.save(using=self._db)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_user(self, email_address: str, password: str | None = None, **extra_fields):
|
||||||
|
extra_fields.setdefault("is_staff", False)
|
||||||
|
return self._create_user(email_address, password, **extra_fields)
|
||||||
|
|
||||||
|
def create_superuser(self, email_address: str, password: str | None = None, **extra_fields):
|
||||||
|
extra_fields.setdefault("is_staff", True)
|
||||||
|
if extra_fields.get("is_staff") is not True:
|
||||||
|
raise ValueError("Superuser must have is_staff=True.")
|
||||||
|
return self._create_user(email_address, password, **extra_fields)
|
||||||
94
apps/accounts/migrations/0001_initial.py
Normal file
94
apps/accounts/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('email_address', models.EmailField(max_length=255, unique=True, verbose_name='Email Address')),
|
||||||
|
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
|
||||||
|
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||||
|
('date_of_birth', models.DateField(blank=True, null=True, verbose_name='Date of Birth')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Account Active')),
|
||||||
|
('is_staff', models.BooleanField(default=False, verbose_name='Account Admin')),
|
||||||
|
('is_manager', models.BooleanField(default=False, verbose_name='Organization Manager')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'User',
|
||||||
|
'verbose_name_plural': 'Users',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Organization',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('name', models.CharField(max_length=255, unique=True, verbose_name='Name')),
|
||||||
|
('description', models.TextField(blank=True, default='', verbose_name='Description')),
|
||||||
|
('members', models.ManyToManyField(related_name='organizations', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owned_organizations', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Organization',
|
||||||
|
'verbose_name_plural': 'Organizations',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Invite',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='Token')),
|
||||||
|
('expires_at', models.DateTimeField(verbose_name='Expires At')),
|
||||||
|
('uses', models.IntegerField(default=0, verbose_name='Uses')),
|
||||||
|
('max_uses', models.IntegerField(default=1, verbose_name='Max Uses')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
||||||
|
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_invites', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invite_tokens', to='accounts.organization')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Invite Token',
|
||||||
|
'verbose_name_plural': 'Invite Tokens',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Role',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True, verbose_name='Name')),
|
||||||
|
('description', models.TextField(blank=True, default='', verbose_name='Description')),
|
||||||
|
('members', models.ManyToManyField(related_name='roles', to=settings.AUTH_USER_MODEL)),
|
||||||
|
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='roles', to='accounts.organization')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Role',
|
||||||
|
'verbose_name_plural': 'Roles',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='invite',
|
||||||
|
options={'verbose_name': 'Invite', 'verbose_name_plural': 'Invites'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='invite',
|
||||||
|
name='organization',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='accounts.organization'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/accounts/migrations/__init__.py
Normal file
0
apps/accounts/migrations/__init__.py
Normal file
19
apps/accounts/mixins.py
Normal file
19
apps/accounts/mixins.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.db.models import BigAutoField, DateTimeField, Model, UUIDField
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
class IdentifierMixin(Model):
|
||||||
|
id = BigAutoField(verbose_name = _("ID"), primary_key = True)
|
||||||
|
uuid = UUIDField(verbose_name = _("UUID"), default = uuid4, editable = False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
class TimeStampMixin(Model):
|
||||||
|
|
||||||
|
created_at = DateTimeField(verbose_name = _("Created At"), auto_now_add = True)
|
||||||
|
updated_at = DateTimeField(verbose_name = _("Updated At"), auto_now = True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
141
apps/accounts/models.py
Normal file
141
apps/accounts/models.py
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
from typing import ClassVar
|
||||||
|
from uuid import uuid4
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import BooleanField, CASCADE, CharField, DateField, DateTimeField, EmailField, ForeignKey, IntegerField, ManyToManyField, Model, TextField, UUIDField
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from apps.accounts.managers import UserManager
|
||||||
|
from apps.accounts.mixins import IdentifierMixin, TimeStampMixin
|
||||||
|
|
||||||
|
class User(AbstractBaseUser, IdentifierMixin, TimeStampMixin, PermissionsMixin):
|
||||||
|
|
||||||
|
email_address = EmailField(verbose_name = _("Email Address"), max_length = 255, unique = True)
|
||||||
|
first_name = CharField(verbose_name = _("First Name"), max_length = 255)
|
||||||
|
last_name = CharField(verbose_name = _("Last Name"), max_length = 255)
|
||||||
|
date_of_birth = DateField(verbose_name = _("Date of Birth"), null = True, blank = True)
|
||||||
|
|
||||||
|
is_active = BooleanField(verbose_name = _("Account Active"), default = True)
|
||||||
|
is_staff = BooleanField(verbose_name = _("Account Admin"), default = False)
|
||||||
|
is_manager = BooleanField(verbose_name = _("Organization Manager"), default = False)
|
||||||
|
|
||||||
|
USERNAME_FIELD = 'email_address'
|
||||||
|
EMAIL_FIELD = 'email_address'
|
||||||
|
REQUIRED_FIELDS = ['first_name', 'last_name', 'date_of_birth']
|
||||||
|
|
||||||
|
objects: ClassVar[UserManager] = UserManager()
|
||||||
|
|
||||||
|
def has_perm(self, perm, obj=None):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_module_perms(self, app_label):
|
||||||
|
return True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('User')
|
||||||
|
verbose_name_plural = _('Users')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_name(self) -> str:
|
||||||
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.full_name
|
||||||
|
|
||||||
|
class Organization(IdentifierMixin, TimeStampMixin, Model):
|
||||||
|
|
||||||
|
name = CharField(verbose_name = _("Name"), max_length = 255, unique = True)
|
||||||
|
|
||||||
|
description = TextField(verbose_name = _("Description"), blank = True, default = '')
|
||||||
|
|
||||||
|
owner = ForeignKey(User, on_delete = CASCADE, related_name = 'owned_organizations')
|
||||||
|
members = ManyToManyField(User, related_name = 'organizations')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Organization')
|
||||||
|
verbose_name_plural = _('Organizations')
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Invite(IdentifierMixin, TimeStampMixin, Model):
|
||||||
|
|
||||||
|
token = UUIDField(verbose_name = _("Token"), default = uuid4, unique = True, editable = False)
|
||||||
|
organization = ForeignKey(Organization, on_delete = CASCADE, related_name = "invites")
|
||||||
|
created_by = ForeignKey(User, on_delete = CASCADE, related_name = "created_invites")
|
||||||
|
expires_at = DateTimeField(verbose_name=_("Expires At"))
|
||||||
|
uses = IntegerField(verbose_name=_("Uses"), default = 0)
|
||||||
|
max_uses = IntegerField(verbose_name=_("Max Uses"), default = 1)
|
||||||
|
is_active = BooleanField(verbose_name=_("Is Active"), default = True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Invite")
|
||||||
|
verbose_name_plural = _("Invites")
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.expires_at:
|
||||||
|
self.expires_at = timezone.now() + timedelta(days=7)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return self.is_active and self.uses < self.max_uses and timezone.now() < self.expires_at
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Invite for {self.organization.name} by {self.created_by.full_name} (expires {self.expires_at})"
|
||||||
|
|
||||||
|
class Role(IdentifierMixin, TimeStampMixin, Model):
|
||||||
|
|
||||||
|
name = CharField(verbose_name = _("Name"), max_length = 100, unique = True)
|
||||||
|
description = TextField(verbose_name = _("Description"), blank = True, default = '')
|
||||||
|
organization = ForeignKey(Organization, on_delete = CASCADE, related_name = "roles")
|
||||||
|
members = ManyToManyField(User, related_name = "roles")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Role')
|
||||||
|
verbose_name_plural = _('Roles')
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.name} ({self.organization.name})"
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Role)
|
||||||
|
def create_default_agents_for_role(sender, instance: Role, created: bool, **kwargs):
|
||||||
|
if created:
|
||||||
|
from apps.onboarding.models import AgentConfig # L: circular import :(
|
||||||
|
|
||||||
|
default_agents = [
|
||||||
|
{
|
||||||
|
'type': 'curriculum',
|
||||||
|
'name': f"{instance.name} Curriculum Agent",
|
||||||
|
'prompt': f"You are a curriculum specialist. Design a learning path for someone in the role of {instance.name}..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'knowledge',
|
||||||
|
'name': f"{instance.name} Knowledge Agent",
|
||||||
|
'prompt': f"You are a knowledge assistant. Search the provided docs and help with questions about {instance.name}..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'assessment',
|
||||||
|
'name': f"{instance.name} Assessment Agent",
|
||||||
|
'prompt': f"You are an evaluator. Create questions based on the knowledge of {instance.name}..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'type': 'monitor',
|
||||||
|
'name': f"{instance.name} Progress Monitor",
|
||||||
|
'prompt': f"You are a supervisor tracking the progress of someone in the role of {instance.name}..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for agent_data in default_agents:
|
||||||
|
AgentConfig.objects.create(
|
||||||
|
organization=instance.organization,
|
||||||
|
name=agent_data['name'],
|
||||||
|
agent_type=agent_data['type'],
|
||||||
|
system_prompt=agent_data['prompt'],
|
||||||
|
llm_config={"model_id": "meta-llama-3.1-8b-instruct"}
|
||||||
|
)
|
||||||
57
apps/accounts/serializers.py
Normal file
57
apps/accounts/serializers.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
|
from apps.accounts.models import Role, User, Organization, Invite
|
||||||
|
|
||||||
|
class UserSerializer(ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['id', 'uuid', 'email_address', 'first_name', 'last_name', 'date_of_birth', 'is_staff', 'is_manager', 'created_at', 'updated_at']
|
||||||
|
read_only_fields = ['id', 'uuid', 'is_staff', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
class OrganizationSerializer(ModelSerializer):
|
||||||
|
owner = UserSerializer(read_only = True)
|
||||||
|
member_count = SerializerMethodField()
|
||||||
|
role_count = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Organization
|
||||||
|
fields = ['id', 'uuid', 'name', 'description', 'owner', 'created_at', 'updated_at', 'member_count', 'role_count']
|
||||||
|
read_only_fields = ['uuid', 'owner', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
def get_member_count(self, obj):
|
||||||
|
return obj.members.count()
|
||||||
|
|
||||||
|
def get_role_count(self, obj):
|
||||||
|
return obj.roles.count()
|
||||||
|
|
||||||
|
class InviteSerializer(ModelSerializer):
|
||||||
|
organization = OrganizationSerializer(read_only = True)
|
||||||
|
created_by = UserSerializer(read_only = True)
|
||||||
|
invite_url = SerializerMethodField()
|
||||||
|
is_valid = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Invite
|
||||||
|
fields = ['id', 'token', 'organization', 'created_by', 'expires_at', 'uses', 'max_uses', 'is_active', 'created_at', 'updated_at', 'invite_url', 'is_valid']
|
||||||
|
read_only_fields = ['id', 'token', 'organization', 'created_by', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
def get_invite_url(self, obj: Invite) -> str:
|
||||||
|
request = self.context.get('request')
|
||||||
|
if request:
|
||||||
|
return request.build_absolute_uri(f'/invite/{obj.token}')
|
||||||
|
return f'/invite/{obj.token}'
|
||||||
|
|
||||||
|
def get_is_valid(self, obj: Invite) -> bool:
|
||||||
|
return obj.is_valid()
|
||||||
|
|
||||||
|
class RoleSerializer(ModelSerializer):
|
||||||
|
organization = OrganizationSerializer(read_only = True)
|
||||||
|
member_count = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Role
|
||||||
|
fields = ['id', 'uuid', 'name', 'description', 'organization', 'created_at', 'updated_at', 'member_count']
|
||||||
|
read_only_fields = ['id', 'uuid', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
def get_member_count(self, obj: Role):
|
||||||
|
return obj.members.count()
|
||||||
269
apps/accounts/viewsets.py
Normal file
269
apps/accounts/viewsets.py
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
from django.contrib.auth import authenticate, login, logout
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_200_OK, HTTP_201_CREATED
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated, IsAuthenticatedOrReadOnly
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
|
from apps.accounts.models import Invite, Organization, Role, User
|
||||||
|
from apps.accounts.serializers import InviteSerializer, OrganizationSerializer, RoleSerializer, UserSerializer
|
||||||
|
|
||||||
|
class UserViewSet(ReadOnlyModelViewSet):
|
||||||
|
|
||||||
|
queryset = User.objects.all()
|
||||||
|
serializer_class = UserSerializer
|
||||||
|
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
|
||||||
|
def login(self, request):
|
||||||
|
email_address = request.data.get('email_address')
|
||||||
|
password = request.data.get('password')
|
||||||
|
if not email_address or not password:
|
||||||
|
return Response({'error': 'Email and password are required'}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
email_address = User.objects.normalize_email(email_address)
|
||||||
|
user = authenticate(request, username=email_address, password=password)
|
||||||
|
if user is None:
|
||||||
|
return Response({'error': 'Invalid credentials'}, status=HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
login(request, user)
|
||||||
|
return Response({'user': UserSerializer(user).data, 'message': 'Login successful', 'success': True}, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
|
||||||
|
def logout(self, request):
|
||||||
|
logout(request)
|
||||||
|
return Response({'message': 'Logout successful', 'success': True}, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
|
||||||
|
def me(self, request):
|
||||||
|
user_data = UserSerializer(request.user).data
|
||||||
|
user_data['success'] = True
|
||||||
|
return Response(user_data)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], permission_classes=[AllowAny])
|
||||||
|
def session(self, request):
|
||||||
|
return Response({'isAuthenticated': request.user.is_authenticated, 'isStaff': request.user.is_staff if request.user.is_authenticated else False})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], permission_classes=[AllowAny])
|
||||||
|
def signup(self, request):
|
||||||
|
try:
|
||||||
|
data = request.data
|
||||||
|
except:
|
||||||
|
return Response({'detail': 'Invalid data provided.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
email_address = data.get('email_address')
|
||||||
|
if not email_address:
|
||||||
|
return Response({'detail': 'Email address is required.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
email_address = User.objects.normalize_email(email_address)
|
||||||
|
if User.objects.filter(email_address=email_address).exists():
|
||||||
|
return Response({'detail': 'Email address already exists.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
if not data.get('first_name') or not data.get('last_name'):
|
||||||
|
return Response({'detail': 'First and last name(s) must be provided.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
if type(manager:=data.get('manager')) is not bool:
|
||||||
|
if manager in ['true', 'True']:
|
||||||
|
manager = True
|
||||||
|
elif manager in ['false', 'False']:
|
||||||
|
manager = False
|
||||||
|
else:
|
||||||
|
return Response({'detail': '"manager" field must be a boolean value.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
if data.get('password') != data.get('confirm_password'):
|
||||||
|
return Response({'detail': 'Passwords do not match.', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
try:
|
||||||
|
user = User.objects.create_user(
|
||||||
|
email_address=email_address,
|
||||||
|
password=data.get('password'),
|
||||||
|
first_name=data.get('first_name'),
|
||||||
|
last_name=data.get('last_name'),
|
||||||
|
date_of_birth=data.get('date_of_birth'),
|
||||||
|
is_manager=manager,
|
||||||
|
)
|
||||||
|
return Response({'detail': 'User account created successfully.', 'success': True}, status=HTTP_201_CREATED)
|
||||||
|
except Exception as e:
|
||||||
|
return Response({'detail': str(e), 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], permission_classes=[IsAuthenticated])
|
||||||
|
def change_password(self, request):
|
||||||
|
data = request.data
|
||||||
|
required_fields = ['old_password', 'password', 'confirm_password']
|
||||||
|
for field in required_fields:
|
||||||
|
if not data.get(field):
|
||||||
|
return Response({'detail': f'"{field}" not provided', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
if data.get('password') != data.get('confirm_password'):
|
||||||
|
return Response({'detail': 'Passwords do not match', 'success': False}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
user = request.user
|
||||||
|
if not user.check_password(data.get('old_password')):
|
||||||
|
return Response({'detail': 'Old password is incorrect', 'success': False}, status=HTTP_401_UNAUTHORIZED)
|
||||||
|
user.set_password(data.get('password'))
|
||||||
|
user.save()
|
||||||
|
return Response({'detail': 'Password changed successfully', 'success': True}, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class OrganizationViewSet(ModelViewSet):
|
||||||
|
queryset = Organization.objects.all()
|
||||||
|
serializer_class = OrganizationSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Organization.objects.filter(
|
||||||
|
Q(owner=self.request.user) | Q(members=self.request.user)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
organization = serializer.save(owner=self.request.user)
|
||||||
|
organization.members.add(self.request.user)
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
if not request.user.is_manager:
|
||||||
|
return Response({'error': 'Forbidden'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['get'], url_path='invite')
|
||||||
|
def list_invites(self, request, uuid=None):
|
||||||
|
organization = self.get_object()
|
||||||
|
invites = organization.invites.all()
|
||||||
|
serializer = InviteSerializer(invites, many=True, context={'request': request})
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path='create-invite')
|
||||||
|
def create_invite(self, request, uuid=None):
|
||||||
|
organization = self.get_object()
|
||||||
|
if not request.user.is_manager:
|
||||||
|
return Response({'error': 'Forbidden'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
max_uses = request.query_params.get('max_uses') or request.data.get('max_uses', 1)
|
||||||
|
invitation = Invite.objects.create(
|
||||||
|
organization=organization,
|
||||||
|
created_by=request.user,
|
||||||
|
max_uses=int(max_uses) if str(max_uses).isdigit() else 1
|
||||||
|
)
|
||||||
|
return Response(InviteSerializer(invitation, context={'request': request}).data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['delete'], url_path=r'revoke-invite/(?P<token>[0-9a-f-]{36})')
|
||||||
|
def revoke_invite(self, request, uuid=None, token=None):
|
||||||
|
organization = self.get_object()
|
||||||
|
if not request.user.is_manager:
|
||||||
|
return Response({'error': 'Only managers can revoke invites'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
invite = organization.invites.filter(token=token).first()
|
||||||
|
if not invite:
|
||||||
|
return Response({'error': 'Invalid invitation token or not found in this organization'}, status=HTTP_404_NOT_FOUND)
|
||||||
|
invite.is_active = False
|
||||||
|
invite.save()
|
||||||
|
return Response({'message': 'Invitation successfully revoked'}, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'], url_path='join/(?P<token>[0-9a-f-]{36})')
|
||||||
|
def join(self, request, token=None):
|
||||||
|
try:
|
||||||
|
invitation = Invite.objects.get(token=token)
|
||||||
|
except Invite.DoesNotExist:
|
||||||
|
return Response({'error': 'Not Found'}, status=HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
if not invitation.is_valid():
|
||||||
|
return Response({'error': 'Invalid or expired token'}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
organization = invitation.organization
|
||||||
|
if organization.members.filter(id=request.user.id).exists():
|
||||||
|
return Response({'error': 'Already a member'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
organization.members.add(request.user)
|
||||||
|
|
||||||
|
invitation.uses += 1
|
||||||
|
if invitation.uses >= invitation.max_uses:
|
||||||
|
invitation.is_active = False
|
||||||
|
invitation.save()
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'message': 'Joined',
|
||||||
|
'organization': OrganizationSerializer(organization).data
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path='leave')
|
||||||
|
def leave(self, request, uuid=None):
|
||||||
|
organization = self.get_object()
|
||||||
|
if organization.owner == request.user:
|
||||||
|
return Response({'error': 'Owner cannot leave'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
if not organization.members.filter(id=request.user.id).exists():
|
||||||
|
return Response({'error': 'Not a member'}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
organization.members.remove(request.user)
|
||||||
|
return Response({'message': 'Left organization'})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['get'], url_path='members')
|
||||||
|
def list_members(self, request, uuid=None):
|
||||||
|
organization = self.get_object()
|
||||||
|
serializer = UserSerializer(organization.members.all(), many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path=r'member/(?P<user_id>\d+)/remove')
|
||||||
|
def remove_member(self, request, uuid=None, user_id=None):
|
||||||
|
if not request.user.is_manager:
|
||||||
|
return Response({'error': 'Forbidden'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
organization = self.get_object()
|
||||||
|
if str(organization.owner.id) == str(user_id):
|
||||||
|
return Response({'error': 'Cannot remove owner'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
user_to_remove = organization.members.filter(id=user_id).first()
|
||||||
|
if not user_to_remove:
|
||||||
|
return Response({'error': 'Not found'}, status=HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
organization.members.remove(user_to_remove)
|
||||||
|
return Response({'message': 'Removed'})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['get', 'post'], url_path='role')
|
||||||
|
def roles(self, request, uuid=None):
|
||||||
|
organization = self.get_object()
|
||||||
|
if request.method == 'GET':
|
||||||
|
return Response(RoleSerializer(organization.roles.all(), many=True).data)
|
||||||
|
|
||||||
|
if not request.user.is_manager:
|
||||||
|
return Response({'error': 'Forbidden'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
name = (request.data.get('name') or '').strip()
|
||||||
|
description = (request.data.get('description') or '').strip()
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return Response({'error': 'Role name is required'}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if organization.roles.filter(name__iexact=name).exists():
|
||||||
|
return Response({'error': 'A role with this name already exists in this organization'}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
role = Role.objects.create(name=name, description=description, organization=organization)
|
||||||
|
return Response(RoleSerializer(role).data, status=HTTP_201_CREATED)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'], url_path='role/mine')
|
||||||
|
def my_roles(self, request):
|
||||||
|
roles = Role.objects.filter(members=request.user).distinct()
|
||||||
|
serializer = RoleSerializer(roles, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['delete'], url_path='role/(?P<role_uuid>[0-9a-f-]{36})')
|
||||||
|
def delete_role(self, request, uuid=None, role_uuid=None):
|
||||||
|
if not request.user.is_manager:
|
||||||
|
return Response({'error': 'Forbidden'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
role = Role.objects.filter(uuid=role_uuid, organization__uuid=uuid)
|
||||||
|
if not role.exists():
|
||||||
|
return Response({'error': 'Not found'}, status=HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
role.delete()
|
||||||
|
return Response(status=HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path='role/(?P<role_uuid>[0-9a-f-]{36})/join')
|
||||||
|
def join_role(self, request, uuid=None, role_uuid=None):
|
||||||
|
organization = self.get_object()
|
||||||
|
|
||||||
|
role = Role.objects.filter(uuid=role_uuid, organization=organization).first()
|
||||||
|
if not role:
|
||||||
|
return Response({'error': 'Role not found'}, status=HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
if not organization.members.filter(id=request.user.id).exists() and organization.owner != request.user:
|
||||||
|
return Response({'error': 'Not a member of this organization'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
if role.members.filter(id=request.user.id).exists():
|
||||||
|
return Response({'message': 'Already a member of this role'}, status=HTTP_200_OK)
|
||||||
|
|
||||||
|
role.members.add(request.user)
|
||||||
|
return Response({'message': 'Joined role successfully'}, status=HTTP_200_OK)
|
||||||
0
apps/knowledge/__init__.py
Normal file
0
apps/knowledge/__init__.py
Normal file
35
apps/knowledge/admin.py
Normal file
35
apps/knowledge/admin.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from apps.knowledge.models import TrainingFile, RoleRagDocument
|
||||||
|
|
||||||
|
@admin.register(TrainingFile)
|
||||||
|
class TrainingFileAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('file_name', 'role', 'status', 'is_processed', 'uploaded_by', 'created_at')
|
||||||
|
list_filter = ('status', 'is_processed', 'role__organization', 'created_at')
|
||||||
|
search_fields = ('file_name', 'role__name', 'uploaded_by__email_address')
|
||||||
|
raw_id_fields = ('role', 'uploaded_by')
|
||||||
|
readonly_fields = ('uuid', 'file_size', 'file_type', 'created_at', 'updated_at')
|
||||||
|
ordering = ('-created_at',)
|
||||||
|
|
||||||
|
@admin.register(RoleRagDocument)
|
||||||
|
class RoleRagDocumentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('role', 'chunk_index', 'training_file', 'is_active', 'created_at')
|
||||||
|
list_filter = ('is_active', 'role__organization', 'created_at')
|
||||||
|
search_fields = ('content', 'role__name', 'training_file__file_name')
|
||||||
|
raw_id_fields = ('role', 'training_file')
|
||||||
|
|
||||||
|
readonly_fields = ('uuid', 'content_hash', 'display_embedding', 'created_at', 'updated_at')
|
||||||
|
ordering = ('role', 'chunk_index')
|
||||||
|
|
||||||
|
def get_fields(self, request, obj=None):
|
||||||
|
fields = super().get_fields(request, obj)
|
||||||
|
if 'embedding' in fields:
|
||||||
|
fields.remove('embedding')
|
||||||
|
return fields
|
||||||
|
|
||||||
|
@admin.display(description=_("Embedding Preview (1536d)"))
|
||||||
|
def display_embedding(self, obj):
|
||||||
|
if obj.embedding is not None:
|
||||||
|
preview = list(obj.embedding[:5])
|
||||||
|
return f"Vector({len(obj.embedding)}): {preview}... [Truncated]"
|
||||||
|
return _("No embedding generated")
|
||||||
5
apps/knowledge/apps.py
Normal file
5
apps/knowledge/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class KnowledgeConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.knowledge'
|
||||||
63
apps/knowledge/migrations/0001_initial.py
Normal file
63
apps/knowledge/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import django.db.models.deletion
|
||||||
|
import pgvector.django
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
from pgvector.django import VectorExtension
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
VectorExtension(),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TrainingFile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('file', models.FileField(upload_to='training_files/%Y/%m/%d/')),
|
||||||
|
('file_name', models.CharField(max_length=255)),
|
||||||
|
('file_size', models.IntegerField()),
|
||||||
|
('file_type', models.CharField(max_length=50)),
|
||||||
|
('description', models.TextField(blank=True, default='')),
|
||||||
|
('status', models.CharField(choices=[('ingesting', 'Ingesting'), ('chunked', 'Chunked'), ('embedded', 'Embedded'), ('failed', 'Failed')], default='ingesting', max_length=20)),
|
||||||
|
('is_processed', models.BooleanField(default=False)),
|
||||||
|
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_files', to='accounts.role')),
|
||||||
|
('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uploaded_training_files', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Training File',
|
||||||
|
'verbose_name_plural': 'Training Files',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RoleRagDocument',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('content', models.TextField()),
|
||||||
|
('content_hash', models.CharField(db_index=True, max_length=64)),
|
||||||
|
('embedding', pgvector.django.VectorField(blank=True, dimensions=1536, null=True)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('chunk_index', models.IntegerField(default=0)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rag_documents', to='accounts.role')),
|
||||||
|
('training_file', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='knowledge.trainingfile')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Role RAG Document',
|
||||||
|
'verbose_name_plural': 'Role RAG Documents',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/knowledge/migrations/__init__.py
Normal file
0
apps/knowledge/migrations/__init__.py
Normal file
75
apps/knowledge/models.py
Normal file
75
apps/knowledge/models.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.db.models import CASCADE, CharField, ForeignKey, IntegerField, TextField, BooleanField, FileField, JSONField, Model
|
||||||
|
from django.db.models.signals import post_delete, post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from pgvector.django import VectorField
|
||||||
|
|
||||||
|
from apps.accounts.mixins import IdentifierMixin, TimeStampMixin
|
||||||
|
from apps.accounts.models import User, Role
|
||||||
|
|
||||||
|
class TrainingFile(IdentifierMixin, TimeStampMixin, Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('ingesting', 'Ingesting'),
|
||||||
|
('chunked', 'Chunked'),
|
||||||
|
('embedded', 'Embedded'),
|
||||||
|
('failed', 'Failed'),
|
||||||
|
]
|
||||||
|
|
||||||
|
role = ForeignKey(Role, on_delete=CASCADE, related_name="training_files")
|
||||||
|
uploaded_by = ForeignKey(User, on_delete=CASCADE, related_name="uploaded_training_files")
|
||||||
|
|
||||||
|
file = FileField(upload_to='training_files/%Y/%m/%d/')
|
||||||
|
file_name = CharField(max_length=255)
|
||||||
|
file_size = IntegerField()
|
||||||
|
file_type = CharField(max_length=50)
|
||||||
|
|
||||||
|
description = TextField(blank=True, default='')
|
||||||
|
status = CharField(max_length=20, choices=STATUS_CHOICES, default='ingesting')
|
||||||
|
is_processed = BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Training File")
|
||||||
|
verbose_name_plural = _("Training Files")
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.file_name} ({self.role.name})"
|
||||||
|
|
||||||
|
|
||||||
|
class RoleRagDocument(IdentifierMixin, TimeStampMixin, Model):
|
||||||
|
|
||||||
|
role = ForeignKey(Role, on_delete=CASCADE, related_name='rag_documents')
|
||||||
|
training_file = ForeignKey(TrainingFile, on_delete=CASCADE, related_name='chunks', null=True, blank=True)
|
||||||
|
|
||||||
|
content = TextField()
|
||||||
|
content_hash = CharField(max_length=64, db_index=True)
|
||||||
|
|
||||||
|
embedding = VectorField(dimensions=1536, null=True, blank=True)
|
||||||
|
|
||||||
|
metadata = JSONField(default=dict, blank=True)
|
||||||
|
chunk_index = IntegerField(default=0)
|
||||||
|
is_active = BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Role RAG Document")
|
||||||
|
verbose_name_plural = _("Role RAG Documents")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.role.name} - Chunk {self.chunk_index}"
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=TrainingFile)
|
||||||
|
def delete_physical_file(sender, instance, **kwargs):
|
||||||
|
if instance.file:
|
||||||
|
if os.path.isfile(instance.file.path):
|
||||||
|
os.remove(instance.file.path)
|
||||||
|
|
||||||
|
@receiver(post_save, sender=TrainingFile)
|
||||||
|
def trigger_ingestion(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
def _enqueue():
|
||||||
|
from apps.knowledge.tasks import ingest_training_file_task # L: circular import :(
|
||||||
|
ingest_training_file_task.delay(str(instance.uuid))
|
||||||
|
transaction.on_commit(_enqueue)
|
||||||
43
apps/knowledge/serializers.py
Normal file
43
apps/knowledge/serializers.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
|
|
||||||
|
from apps.accounts.serializers import RoleSerializer, UserSerializer
|
||||||
|
from apps.knowledge.models import TrainingFile, RoleRagDocument
|
||||||
|
|
||||||
|
class TrainingFileSerializer(ModelSerializer):
|
||||||
|
uploaded_by = UserSerializer(read_only=True)
|
||||||
|
role = RoleSerializer(read_only=True)
|
||||||
|
file_url = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = TrainingFile
|
||||||
|
fields = [
|
||||||
|
'id', 'uuid', 'role', 'uploaded_by', 'file', 'file_url',
|
||||||
|
'file_name', 'file_size', 'file_type', 'description',
|
||||||
|
'status', 'is_processed', 'created_at', 'updated_at'
|
||||||
|
]
|
||||||
|
read_only_fields = [
|
||||||
|
'id', 'uuid', 'uploaded_by', 'file_size', 'file_type',
|
||||||
|
'status', 'is_processed', 'created_at', 'updated_at',
|
||||||
|
'role'
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_file_url(self, obj: TrainingFile) -> str:
|
||||||
|
request = self.context.get('request')
|
||||||
|
if obj.file and request:
|
||||||
|
return request.build_absolute_uri(obj.file.url)
|
||||||
|
return obj.file.url if obj.file else None
|
||||||
|
|
||||||
|
class RoleRagDocumentSerializer(ModelSerializer):
|
||||||
|
training_file_name = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RoleRagDocument
|
||||||
|
fields = [
|
||||||
|
'id', 'uuid', 'role', 'training_file', 'training_file_name',
|
||||||
|
'content', 'content_hash', 'metadata', 'chunk_index',
|
||||||
|
'is_active', 'created_at'
|
||||||
|
]
|
||||||
|
read_only_fields = ['id', 'uuid', 'content_hash', 'created_at']
|
||||||
|
|
||||||
|
def get_training_file_name(self, obj: RoleRagDocument) -> str:
|
||||||
|
return obj.training_file.file_name if obj.training_file else None
|
||||||
103
apps/knowledge/tasks.py
Normal file
103
apps/knowledge/tasks.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import httpx
|
||||||
|
import hashlib
|
||||||
|
from pypdf import PdfReader
|
||||||
|
from docx import Document
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
from django.db import transaction
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .models import TrainingFile, RoleRagDocument
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_text_bytes(raw_bytes: bytes) -> str:
|
||||||
|
try:
|
||||||
|
return raw_bytes.decode('utf-8')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return raw_bytes.decode('latin-1', errors='ignore')
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_text_from_training_file(file_obj: TrainingFile) -> str:
|
||||||
|
file_name = (file_obj.file_name or '').lower()
|
||||||
|
|
||||||
|
if file_name.endswith('.pdf'):
|
||||||
|
with file_obj.file.open('rb') as f:
|
||||||
|
reader = PdfReader(f)
|
||||||
|
pages = [page.extract_text() or '' for page in reader.pages]
|
||||||
|
return '\n'.join(pages).strip()
|
||||||
|
|
||||||
|
if file_name.endswith('.docx'):
|
||||||
|
with file_obj.file.open('rb') as f:
|
||||||
|
document = Document(f)
|
||||||
|
paragraphs = [paragraph.text for paragraph in document.paragraphs if paragraph.text]
|
||||||
|
return '\n'.join(paragraphs).strip()
|
||||||
|
|
||||||
|
with file_obj.file.open('rb') as f:
|
||||||
|
raw_bytes = f.read()
|
||||||
|
return _decode_text_bytes(raw_bytes).strip()
|
||||||
|
|
||||||
|
def _get_text_chunks(text: str, size: int = 10000):
|
||||||
|
"""Slices text into rough blocks to prevent HTTP timeouts."""
|
||||||
|
for i in range(0, len(text), size):
|
||||||
|
yield text[i:i + size]
|
||||||
|
|
||||||
|
@shared_task(name="apps.knowledge.tasks.ingest_training_file_task", bind=True, soft_time_limit=900, time_limit=1200)
|
||||||
|
def ingest_training_file_task(self, file_uuid):
|
||||||
|
try:
|
||||||
|
file_obj = TrainingFile.objects.get(uuid=file_uuid)
|
||||||
|
except TrainingFile.DoesNotExist:
|
||||||
|
return f"File {file_uuid} not found."
|
||||||
|
|
||||||
|
file_obj.status = 'ingesting'
|
||||||
|
file_obj.save()
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_text = _extract_text_from_training_file(file_obj)
|
||||||
|
if not raw_text:
|
||||||
|
raise ValueError('No extractable text found.')
|
||||||
|
|
||||||
|
all_documents = []
|
||||||
|
chunk_counter = 0
|
||||||
|
|
||||||
|
|
||||||
|
timeout = httpx.Timeout(60.0)
|
||||||
|
|
||||||
|
with httpx.Client(timeout=timeout) as client:
|
||||||
|
|
||||||
|
for text_segment in _get_text_chunks(raw_text):
|
||||||
|
response = client.post(
|
||||||
|
f"{settings.INFERENCE_URL}/v1/semantic-chunk",
|
||||||
|
json={"text": text_segment, "threshold": 95}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
chunks = result['chunks']
|
||||||
|
embeddings = result['embeddings']
|
||||||
|
|
||||||
|
for chunk_text, embedding in zip(chunks, embeddings):
|
||||||
|
all_documents.append(RoleRagDocument(
|
||||||
|
role=file_obj.role,
|
||||||
|
training_file=file_obj,
|
||||||
|
content=chunk_text,
|
||||||
|
content_hash=hashlib.sha256(chunk_text.encode('utf-8')).hexdigest(),
|
||||||
|
embedding=embedding,
|
||||||
|
chunk_index=chunk_counter,
|
||||||
|
metadata={"source": file_obj.file_name}
|
||||||
|
))
|
||||||
|
chunk_counter += 1
|
||||||
|
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
RoleRagDocument.objects.bulk_create(all_documents)
|
||||||
|
|
||||||
|
file_obj.status = 'embedded'
|
||||||
|
file_obj.is_processed = True
|
||||||
|
file_obj.save()
|
||||||
|
return f"Processed {chunk_counter} chunks via batching."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
file_obj.status = 'failed'
|
||||||
|
file_obj.description = str(e)
|
||||||
|
file_obj.save()
|
||||||
|
raise e
|
||||||
72
apps/knowledge/viewsets.py
Normal file
72
apps/knowledge/viewsets.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
from django.db.models import Q
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.parsers import FormParser, MultiPartParser
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
|
from apps.accounts.models import Role
|
||||||
|
from apps.knowledge.models import RoleRagDocument, TrainingFile
|
||||||
|
from apps.knowledge.serializers import RoleRagDocumentSerializer, TrainingFileSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class TrainingFileViewSet(ModelViewSet):
|
||||||
|
queryset = TrainingFile.objects.all()
|
||||||
|
serializer_class = TrainingFileSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [MultiPartParser, FormParser]
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
return TrainingFile.objects.filter(
|
||||||
|
Q(role__organization__owner=user) |
|
||||||
|
Q(role__organization__members=user)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
role_uuid = self.request.data.get('role')
|
||||||
|
|
||||||
|
try:
|
||||||
|
role = Role.objects.get(uuid=role_uuid)
|
||||||
|
except Role.DoesNotExist:
|
||||||
|
return Response({'error': 'Role not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
is_owner = role.organization.owner == self.request.user
|
||||||
|
is_member = role.organization.members.filter(id=self.request.user.id).exists()
|
||||||
|
|
||||||
|
if not (is_owner or is_member):
|
||||||
|
return Response({'error': 'Permission denied'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
serializer.save(
|
||||||
|
uploaded_by=self.request.user,
|
||||||
|
role=role,
|
||||||
|
file_name=self.request.FILES['file'].name,
|
||||||
|
file_size=self.request.FILES['file'].size,
|
||||||
|
file_type=self.request.FILES['file'].content_type
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
|
||||||
|
is_uploader = instance.uploaded_by == request.user
|
||||||
|
is_org_owner = instance.role.organization.owner == request.user
|
||||||
|
|
||||||
|
if not (is_uploader or is_org_owner or request.user.is_manager):
|
||||||
|
return Response({'error': 'Permission denied'}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleRagDocumentViewSet(ReadOnlyModelViewSet):
|
||||||
|
queryset = RoleRagDocument.objects.all()
|
||||||
|
serializer_class = RoleRagDocumentSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
return RoleRagDocument.objects.filter(
|
||||||
|
Q(role__organization__owner=user) |
|
||||||
|
Q(role__organization__members=user)
|
||||||
|
).distinct()
|
||||||
0
apps/onboarding/__init__.py
Normal file
0
apps/onboarding/__init__.py
Normal file
53
apps/onboarding/admin.py
Normal file
53
apps/onboarding/admin.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from apps.onboarding.models import AgentConfig, OnboardingSession, AgentInteractionLog, OnboardingFlow
|
||||||
|
|
||||||
|
class AgentInteractionLogInline(admin.TabularInline):
|
||||||
|
model = AgentInteractionLog
|
||||||
|
extra = 0
|
||||||
|
readonly_fields = ('sender_type', 'content', 'tool_call_metadata', 'created_at')
|
||||||
|
can_delete = False
|
||||||
|
|
||||||
|
def has_add_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@admin.register(AgentConfig)
|
||||||
|
class AgentConfigAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'agent_type', 'organization', 'created_at')
|
||||||
|
list_filter = ('agent_type', 'organization')
|
||||||
|
search_fields = ('name', 'system_prompt')
|
||||||
|
readonly_fields = ('uuid', 'created_at', 'updated_at')
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('name', 'agent_type', 'organization', 'uuid', 'system_prompt', 'llm_config', 'tool_permissions')}),
|
||||||
|
(_('Agent Logic'), {'fields': ()}),
|
||||||
|
(_('Metadata'), {'fields': ('created_at', 'updated_at')}),
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.register(OnboardingSession)
|
||||||
|
class OnboardingSessionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'role', 'status', 'created_at', 'completed_at')
|
||||||
|
list_filter = ('status', 'role', 'created_at')
|
||||||
|
search_fields = ('user__email_address', 'role__name')
|
||||||
|
readonly_fields = ('uuid', 'created_at', 'updated_at')
|
||||||
|
inlines = [AgentInteractionLogInline]
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('user', 'role', 'status', 'uuid')}),
|
||||||
|
(_('Live State'), {'fields': ('state', 'active_configs')}),
|
||||||
|
(_('Timestamps'), {'fields': ('completed_at', 'created_at', 'updated_at')}),
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.register(AgentInteractionLog)
|
||||||
|
class AgentInteractionLogAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('session', 'sender_type', 'agent_config', 'created_at')
|
||||||
|
list_filter = ('sender_type', 'created_at')
|
||||||
|
search_fields = ('content', 'session__user__email_address')
|
||||||
|
readonly_fields = ('uuid', 'created_at')
|
||||||
|
|
||||||
|
@admin.register(OnboardingFlow)
|
||||||
|
class OnboardingFlowAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('title', 'role', 'is_active', 'created_at')
|
||||||
|
list_filter = ('is_active', 'role')
|
||||||
|
search_fields = ('title', 'role__name')
|
||||||
|
readonly_fields = ('uuid', 'created_at', 'updated_at')
|
||||||
5
apps/onboarding/apps.py
Normal file
5
apps/onboarding/apps.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class OnboardingConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'apps.onboarding'
|
||||||
413
apps/onboarding/consumers.py
Normal file
413
apps/onboarding/consumers.py
Normal file
|
|
@ -0,0 +1,413 @@
|
||||||
|
import json
|
||||||
|
import httpx
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from uuid import uuid4
|
||||||
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
from channels.db import database_sync_to_async
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from apps.onboarding.mcp import MCPRouter
|
||||||
|
from apps.onboarding.models import AgentConfig, OnboardingFlow, OnboardingSession
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class OnboardingConsumer(AsyncWebsocketConsumer):
|
||||||
|
async def connect(self):
|
||||||
|
self.user = self.scope["user"]
|
||||||
|
|
||||||
|
self.context_uuid = self.scope["url_route"]["kwargs"].get("session_uuid")
|
||||||
|
|
||||||
|
if not self.user.is_authenticated:
|
||||||
|
await self.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.router = MCPRouter()
|
||||||
|
await self.accept()
|
||||||
|
|
||||||
|
async def disconnect(self, close_code):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _build_system_prompt(self, config):
|
||||||
|
base_prompt = config.system_prompt or "You are a helpful onboarding assistant."
|
||||||
|
permissions = config.tool_permissions or []
|
||||||
|
if permissions:
|
||||||
|
return f"{base_prompt}\n\nAllowed tools: {', '.join(str(p) for p in permissions)}"
|
||||||
|
return base_prompt
|
||||||
|
|
||||||
|
async def receive(self, text_data):
|
||||||
|
try:
|
||||||
|
data = json.loads(text_data)
|
||||||
|
action = data.get("action")
|
||||||
|
|
||||||
|
if action == "start_full_onboarding":
|
||||||
|
role_uuid = data.get("role_uuid")
|
||||||
|
if not role_uuid:
|
||||||
|
await self.send_log("error", "Missing role_uuid for full onboarding generation")
|
||||||
|
return
|
||||||
|
await self.run_full_onboarding_generation(role_uuid)
|
||||||
|
elif action == "progress_monitor":
|
||||||
|
role_uuid = data.get("role_uuid") or self.context_uuid
|
||||||
|
if not role_uuid:
|
||||||
|
await self.send_log("error", "Missing role_uuid for progress monitoring")
|
||||||
|
return
|
||||||
|
await self.run_progress_monitor(role_uuid)
|
||||||
|
else:
|
||||||
|
|
||||||
|
user_message = data.get("query") or data.get("message")
|
||||||
|
if not user_message:
|
||||||
|
await self.send_log("error", "Missing query/message payload")
|
||||||
|
return
|
||||||
|
config = await self.get_config(self.context_uuid)
|
||||||
|
ai_response = await self.orchestrate_ai(user_message, config)
|
||||||
|
|
||||||
|
await self.send(json.dumps({
|
||||||
|
"type": "completed",
|
||||||
|
"timestamp": timezone.now().isoformat(),
|
||||||
|
"message": "Inference complete.",
|
||||||
|
"content": {
|
||||||
|
"response": ai_response,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"WebSocket Receive Error: {str(e)}")
|
||||||
|
await self.send_log("error", f"Consumer encountered an error: {str(e)}")
|
||||||
|
|
||||||
|
async def run_full_onboarding_generation(self, role_uuid):
|
||||||
|
"""
|
||||||
|
The Master Script that builds the JSON structure sequentially.
|
||||||
|
Pipeline: Curriculum Agent -> Knowledge Agent -> Assessment Agent
|
||||||
|
"""
|
||||||
|
|
||||||
|
await self.send_log("status", "Phase 1: Generating Curriculum...", "curriculum")
|
||||||
|
ca_config = await self.get_config_by_type(role_uuid, 'curriculum')
|
||||||
|
if not ca_config:
|
||||||
|
await self.send_log("error", "Missing curriculum AgentConfig for this role")
|
||||||
|
return
|
||||||
|
|
||||||
|
ca_prompt = (
|
||||||
|
"Based on available documentation, create an onboarding curriculum for this role. "
|
||||||
|
"Output ONLY a valid JSON array of 3-5 strings representing module titles. "
|
||||||
|
"Example: [\"Introduction\", \"Safety\", \"Operations\"]"
|
||||||
|
)
|
||||||
|
ca_response = await self.orchestrate_ai(ca_prompt, ca_config)
|
||||||
|
topics = self._extract_json_list(ca_response)
|
||||||
|
if not topics:
|
||||||
|
await self.send_log("error", "Curriculum generation returned no topics")
|
||||||
|
return
|
||||||
|
|
||||||
|
toc_lines = [f"{idx + 1}. {title}" for idx, title in enumerate(topics)]
|
||||||
|
toc_markdown = "## Table of Contents\n\n" + "\n".join(toc_lines)
|
||||||
|
|
||||||
|
full_structure = []
|
||||||
|
|
||||||
|
|
||||||
|
for index, topic in enumerate(topics):
|
||||||
|
|
||||||
|
await self.send_log("status", f"Phase 2: Researching {topic}...", "knowledge")
|
||||||
|
ka_config = await self.get_config_by_type(role_uuid, 'knowledge')
|
||||||
|
if not ka_config:
|
||||||
|
await self.send_log("error", "Missing knowledge AgentConfig for this role")
|
||||||
|
return
|
||||||
|
|
||||||
|
knowledge_hits = await self.fetch_knowledge_context(role_uuid, topic)
|
||||||
|
context_markdown = self.format_knowledge_context(knowledge_hits)
|
||||||
|
|
||||||
|
page_content = await self.orchestrate_ai(
|
||||||
|
(
|
||||||
|
f"Write a practical onboarding training guide for the topic '{topic}'. "
|
||||||
|
"Use the MCP search context provided below as the primary source. "
|
||||||
|
"If the context is empty, provide a concise best-practice overview and clearly say no indexed documents were found. "
|
||||||
|
"Use Markdown formatting and do NOT include a table of contents in this section.\n\n"
|
||||||
|
f"Role UUID: {role_uuid}\n"
|
||||||
|
f"MCP search context:\n{context_markdown}"
|
||||||
|
),
|
||||||
|
ka_config
|
||||||
|
)
|
||||||
|
|
||||||
|
if index == 0:
|
||||||
|
page_content = f"{toc_markdown}\n\n---\n\n{page_content}"
|
||||||
|
|
||||||
|
|
||||||
|
await self.send_log("status", f"Phase 3: Creating quiz for {topic}...", "assessment")
|
||||||
|
aa_config = await self.get_config_by_type(role_uuid, 'assessment')
|
||||||
|
if not aa_config:
|
||||||
|
await self.send_log("error", "Missing assessment AgentConfig for this role")
|
||||||
|
return
|
||||||
|
aa_prompt = (
|
||||||
|
f"Based on this content: '{page_content[:1000]}', create 2 multiple choice questions. "
|
||||||
|
"Output ONLY a JSON array of objects with keys: 'key', 'label', 'field_type' (use 'select'), "
|
||||||
|
"'options' (array of strings), and 'required' (true)."
|
||||||
|
)
|
||||||
|
quiz_response = await self.orchestrate_ai(aa_prompt, aa_config)
|
||||||
|
quiz_fields = self._extract_json_list(quiz_response)
|
||||||
|
|
||||||
|
|
||||||
|
full_structure.append({
|
||||||
|
"title": topic,
|
||||||
|
"body": page_content,
|
||||||
|
"order": index,
|
||||||
|
"fields": quiz_fields
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
await self.save_full_flow(role_uuid, full_structure)
|
||||||
|
|
||||||
|
|
||||||
|
await self.send(json.dumps({
|
||||||
|
"type": "completed",
|
||||||
|
"timestamp": timezone.now().isoformat(),
|
||||||
|
"message": "Onboarding pipeline complete and structure saved."
|
||||||
|
}))
|
||||||
|
|
||||||
|
async def run_progress_monitor(self, role_uuid):
|
||||||
|
await self.send_log("status", "Progress Monitor is analyzing your onboarding progress...", "monitor")
|
||||||
|
|
||||||
|
monitor_config = await self.get_config_by_type(role_uuid, 'monitor')
|
||||||
|
if not monitor_config:
|
||||||
|
await self.send_log("error", "Missing Progress Monitor AgentConfig for this role")
|
||||||
|
return
|
||||||
|
|
||||||
|
progress_context = await self.get_role_progress_context(role_uuid, self.user.id)
|
||||||
|
|
||||||
|
monitor_prompt = (
|
||||||
|
"You are a progress monitoring agent for onboarding. "
|
||||||
|
"Analyze the role onboarding data below and provide concise feedback with:\n"
|
||||||
|
"1) current status\n2) strengths\n3) gaps\n4) next actions\n"
|
||||||
|
"Keep it short and practical.\n\n"
|
||||||
|
f"Progress context JSON:\n{json.dumps(progress_context)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
feedback = await self.orchestrate_ai(monitor_prompt, monitor_config)
|
||||||
|
|
||||||
|
await self.send(json.dumps({
|
||||||
|
"type": "completed",
|
||||||
|
"timestamp": timezone.now().isoformat(),
|
||||||
|
"message": "Progress analysis complete.",
|
||||||
|
"content": {
|
||||||
|
"role_uuid": role_uuid,
|
||||||
|
"feedback": feedback,
|
||||||
|
"status": progress_context.get("latest_status", "unknown"),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
async def orchestrate_ai(self, user_message, config):
|
||||||
|
"""
|
||||||
|
Handles the multi-turn ReAct loop (Reasoning + Tool Use).
|
||||||
|
"""
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": self._build_system_prompt(config)},
|
||||||
|
{"role": "user", "content": user_message}
|
||||||
|
]
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
for turn in range(5):
|
||||||
|
await self.send_log("thought", f"Agent is thinking (Turn {turn+1})...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await client.post(
|
||||||
|
f"{settings.INFERENCE_URL}/v1/chat/completions",
|
||||||
|
json={
|
||||||
|
"model": config.llm_config.get("model_id", "meta-llama-3.1-8b"),
|
||||||
|
"messages": messages,
|
||||||
|
"tools": self.router.get_tool_definitions(),
|
||||||
|
"tool_choice": "auto"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
res_json = response.json()
|
||||||
|
|
||||||
|
ai_message = res_json["choices"][0]["message"]
|
||||||
|
messages.append(ai_message)
|
||||||
|
|
||||||
|
if ai_message.get("tool_calls"):
|
||||||
|
for tool_call in ai_message["tool_calls"]:
|
||||||
|
fn_name = tool_call["function"]["name"]
|
||||||
|
fn_args = json.loads(tool_call["function"]["arguments"])
|
||||||
|
|
||||||
|
await self.send(json.dumps({
|
||||||
|
"type": "tool_start",
|
||||||
|
"message": f"Accessing knowledge base: {fn_name}...",
|
||||||
|
"content": fn_args
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
result = await self.router.handle_tool_call(fn_name, fn_args)
|
||||||
|
|
||||||
|
messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call["id"],
|
||||||
|
"name": fn_name,
|
||||||
|
"content": json.dumps(result)
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
return ai_message["content"]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await self.send_log("error", f"Inference failed: {str(e)}")
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_knowledge_context(self, role_uuid, topic):
|
||||||
|
query = f"onboarding training content for {topic}"
|
||||||
|
await self.send(json.dumps({
|
||||||
|
"type": "tool_start",
|
||||||
|
"message": "Accessing knowledge base: search_knowledge...",
|
||||||
|
"content": {"query": query, "role_uuid": role_uuid}
|
||||||
|
}))
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self.router.handle_tool_call(
|
||||||
|
"search_knowledge",
|
||||||
|
{
|
||||||
|
"query": query,
|
||||||
|
"role_uuid": role_uuid,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.send(json.dumps({
|
||||||
|
"type": "tool_result",
|
||||||
|
"message": f"Retrieved {len(result) if isinstance(result, list) else 0} knowledge chunk(s)",
|
||||||
|
"content": result,
|
||||||
|
"timestamp": timezone.now().isoformat(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
return result if isinstance(result, list) else []
|
||||||
|
except Exception as exc:
|
||||||
|
await self.send_log("error", f"Knowledge retrieval failed for topic '{topic}': {str(exc)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def format_knowledge_context(self, knowledge_hits):
|
||||||
|
if not knowledge_hits:
|
||||||
|
return "No indexed MCP documents found for this role/topic."
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for idx, item in enumerate(knowledge_hits[:5]):
|
||||||
|
source = item.get("source", "Unknown Source") if isinstance(item, dict) else "Unknown Source"
|
||||||
|
relevance = item.get("relevance") if isinstance(item, dict) else None
|
||||||
|
content = item.get("content", "") if isinstance(item, dict) else ""
|
||||||
|
safe_content = str(content).strip()[:1600]
|
||||||
|
lines.append(
|
||||||
|
f"[{idx + 1}] Source: {source} | Relevance: {relevance}\n{safe_content}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n\n".join(lines)
|
||||||
|
|
||||||
|
def _extract_json_list(self, text):
|
||||||
|
"""Regex helper to pull JSON out of LLM conversational filler."""
|
||||||
|
try:
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
match = re.search(r'\[.*\]', text, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
return json.loads(match.group())
|
||||||
|
return []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _normalize_structure(self, structure):
|
||||||
|
normalized_pages = []
|
||||||
|
for index, page in enumerate(structure or []):
|
||||||
|
fields = []
|
||||||
|
for field_index, field in enumerate(page.get('fields', []) if isinstance(page, dict) else []):
|
||||||
|
if not isinstance(field, dict):
|
||||||
|
continue
|
||||||
|
key = str(field.get('key') or f'field_{field_index + 1}')
|
||||||
|
fields.append({
|
||||||
|
'uuid': str(uuid4()),
|
||||||
|
'key': key,
|
||||||
|
'label': str(field.get('label') or key.replace('_', ' ').title()),
|
||||||
|
'field_type': str(field.get('field_type') or 'text'),
|
||||||
|
'required': bool(field.get('required', False)),
|
||||||
|
'options': field.get('options') if isinstance(field.get('options'), list) else [],
|
||||||
|
'default_value': field.get('default_value', ''),
|
||||||
|
})
|
||||||
|
|
||||||
|
page_title = page.get('title') if isinstance(page, dict) else None
|
||||||
|
page_body = page.get('body') if isinstance(page, dict) else ''
|
||||||
|
page_order = page.get('order') if isinstance(page, dict) else index
|
||||||
|
normalized_pages.append({
|
||||||
|
'uuid': str(uuid4()),
|
||||||
|
'title': str(page_title or f'Module {index + 1}'),
|
||||||
|
'body': str(page_body or ''),
|
||||||
|
'order': int(page_order if isinstance(page_order, int) else index),
|
||||||
|
'fields': fields,
|
||||||
|
})
|
||||||
|
return normalized_pages
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def save_full_flow(self, role_uuid, structure):
|
||||||
|
"""Saves the final nested structure to the OnboardingFlow model."""
|
||||||
|
from apps.accounts.models import Role
|
||||||
|
role = Role.objects.get(uuid=role_uuid)
|
||||||
|
normalized_structure = self._normalize_structure(structure)
|
||||||
|
flow, _ = OnboardingFlow.objects.update_or_create(
|
||||||
|
role=role,
|
||||||
|
defaults={
|
||||||
|
'title': f"AI Onboarding: {role.name}",
|
||||||
|
'structure': normalized_structure,
|
||||||
|
'is_active': True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return flow
|
||||||
|
|
||||||
|
async def send_log(self, log_type, message, content=None):
|
||||||
|
await self.send(json.dumps({
|
||||||
|
"type": log_type,
|
||||||
|
"message": message,
|
||||||
|
"content": content,
|
||||||
|
"timestamp": timezone.now().isoformat()
|
||||||
|
}))
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def get_config(self, config_uuid):
|
||||||
|
return AgentConfig.objects.get(uuid=config_uuid)
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def get_config_by_type(self, role_uuid, agent_type):
|
||||||
|
return AgentConfig.objects.filter(
|
||||||
|
organization__roles__uuid=role_uuid,
|
||||||
|
agent_type=agent_type,
|
||||||
|
).order_by('-updated_at').first()
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def get_role_progress_context(self, role_uuid, user_id):
|
||||||
|
from apps.accounts.models import Role
|
||||||
|
|
||||||
|
role = Role.objects.get(uuid=role_uuid)
|
||||||
|
sessions = OnboardingSession.objects.filter(user_id=user_id, role=role).order_by('-updated_at')
|
||||||
|
latest_session = sessions.first()
|
||||||
|
active_flow = OnboardingFlow.objects.filter(role=role, is_active=True).order_by('-updated_at').first()
|
||||||
|
|
||||||
|
if not latest_session:
|
||||||
|
return {
|
||||||
|
"role_uuid": str(role.uuid),
|
||||||
|
"role_name": role.name,
|
||||||
|
"latest_status": "not_started",
|
||||||
|
"session_count": 0,
|
||||||
|
"flow_exists": bool(active_flow),
|
||||||
|
"progress": 0,
|
||||||
|
"responses_count": 0,
|
||||||
|
"completed_modules": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
state = latest_session.state or {}
|
||||||
|
responses = state.get("responses", {})
|
||||||
|
completed_modules = state.get("completed_modules", [])
|
||||||
|
progress = state.get("progress_percentage", state.get("progress", 0))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"role_uuid": str(role.uuid),
|
||||||
|
"role_name": role.name,
|
||||||
|
"latest_status": latest_session.status,
|
||||||
|
"session_count": sessions.count(),
|
||||||
|
"flow_exists": bool(active_flow),
|
||||||
|
"progress": progress,
|
||||||
|
"responses_count": len(responses) if isinstance(responses, dict) else 0,
|
||||||
|
"completed_modules": completed_modules if isinstance(completed_modules, list) else [],
|
||||||
|
"updated_at": latest_session.updated_at.isoformat() if latest_session.updated_at else None,
|
||||||
|
}
|
||||||
102
apps/onboarding/mcp.py
Normal file
102
apps/onboarding/mcp.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import httpx
|
||||||
|
from channels.db import database_sync_to_async
|
||||||
|
from django.conf import settings
|
||||||
|
from pgvector.django import CosineDistance
|
||||||
|
|
||||||
|
from apps.knowledge.models import RoleRagDocument
|
||||||
|
from apps.onboarding.models import OnboardingSession
|
||||||
|
|
||||||
|
class MCPRouter:
|
||||||
|
|
||||||
|
def get_tool_definitions(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": "search_knowledge",
|
||||||
|
"description": "Search the RAG database for role-specific training content.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {"type": "string"},
|
||||||
|
"role_uuid": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["query", "role_uuid"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update_progress",
|
||||||
|
"description": "Update the user's score or current module in their session.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"session_uuid": {"type": "string"},
|
||||||
|
"score": {"type": "integer"},
|
||||||
|
"completed_module": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["session_uuid"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
async def handle_tool_call(self, name, arguments):
|
||||||
|
if name == "search_knowledge":
|
||||||
|
return await self._search_knowledge(arguments)
|
||||||
|
elif name == "update_progress":
|
||||||
|
return await self._update_progress(arguments)
|
||||||
|
return {"error": f"Tool {name} not found"}
|
||||||
|
|
||||||
|
async def _get_embedding(self, text):
|
||||||
|
"""Fetch embedding from the GPU node."""
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{settings.INFERENCE_URL}/v1/embeddings",
|
||||||
|
json={"input": text}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json()["data"][0]["embedding"]
|
||||||
|
|
||||||
|
async def _search_knowledge(self, args):
|
||||||
|
query = args.get("query")
|
||||||
|
role_uuid = args.get("role_uuid")
|
||||||
|
|
||||||
|
if not query or not role_uuid:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
query_vector = await self._get_embedding(query)
|
||||||
|
|
||||||
|
return await self._search_knowledge_documents(role_uuid, query_vector)
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def _search_knowledge_documents(self, role_uuid, query_vector):
|
||||||
|
|
||||||
|
|
||||||
|
docs = RoleRagDocument.objects.filter(
|
||||||
|
role__uuid=role_uuid,
|
||||||
|
is_active=True
|
||||||
|
).annotate(
|
||||||
|
distance=CosineDistance('embedding', query_vector)
|
||||||
|
).order_by('distance')[:5]
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"content": d.content,
|
||||||
|
"source": d.metadata.get("file_name", "Unknown Source"),
|
||||||
|
"relevance": round(1 - d.distance, 4)
|
||||||
|
}
|
||||||
|
for d in docs
|
||||||
|
]
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def _update_progress(self, args):
|
||||||
|
session = OnboardingSession.objects.get(uuid=args.get("session_uuid"))
|
||||||
|
|
||||||
|
state = session.state or {}
|
||||||
|
if "score" in args:
|
||||||
|
state["last_score"] = args["score"]
|
||||||
|
if "completed_module" in args:
|
||||||
|
state.setdefault("completed_modules", []).append(args["completed_module"])
|
||||||
|
|
||||||
|
session.state = state
|
||||||
|
session.save()
|
||||||
|
return {"status": "success", "new_state": state}
|
||||||
89
apps/onboarding/migrations/0001_initial.py
Normal file
89
apps/onboarding/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AgentConfig',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='Agent Name')),
|
||||||
|
('agent_type', models.CharField(choices=[('curriculum', 'Curriculum Agent (CA)'), ('knowledge', 'Knowledge Agent (KA)'), ('assessment', 'Assessment Agent (AA)'), ('monitor', 'Progress Monitor Agent (PMA)')], max_length=40, verbose_name='Agent Type')),
|
||||||
|
('llm_config', models.JSONField(default=dict, verbose_name='LLM Configuration')),
|
||||||
|
('system_prompt', models.TextField(verbose_name='System Prompt')),
|
||||||
|
('tool_permissions', models.JSONField(default=list, verbose_name='Tool Permissions')),
|
||||||
|
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_configs', to='accounts.organization', verbose_name='Organization')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Agent Config',
|
||||||
|
'verbose_name_plural': 'Agent Configs',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OnboardingFlow',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='Flow Title')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
|
||||||
|
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flows', to='accounts.role', verbose_name='Role')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Onboarding Flow',
|
||||||
|
'verbose_name_plural': 'Onboarding Flows',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OnboardingSession',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('status', models.CharField(choices=[('active', 'Active'), ('completed', 'Completed'), ('paused', 'Paused')], default='active', max_length=20, verbose_name='Session Status')),
|
||||||
|
('state', models.JSONField(blank=True, default=dict, verbose_name='Session State')),
|
||||||
|
('active_configs', models.JSONField(default=dict, verbose_name='Active Configs')),
|
||||||
|
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Completed At')),
|
||||||
|
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='onboarding_sessions', to='accounts.role', verbose_name='Target Role')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='onboarding_sessions', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Onboarding Session',
|
||||||
|
'verbose_name_plural': 'Onboarding Sessions',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AgentInteractionLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('sender_type', models.CharField(choices=[('system', 'System'), ('ai', 'AI'), ('user', 'User'), ('tool', 'Tool Output')], max_length=20, verbose_name='Sender Type')),
|
||||||
|
('content', models.TextField(verbose_name='Message Content')),
|
||||||
|
('tool_call_metadata', models.JSONField(blank=True, default=dict, verbose_name='Tool Call Metadata')),
|
||||||
|
('agent_config', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='onboarding.agentconfig', verbose_name='Agent Config')),
|
||||||
|
('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='onboarding.onboardingsession', verbose_name='Session')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Interaction Log',
|
||||||
|
'verbose_name_plural': 'Interaction Logs',
|
||||||
|
'ordering': ['created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('onboarding', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='agentconfig',
|
||||||
|
name='llm_config',
|
||||||
|
field=models.JSONField(blank=True, default=dict, null=True, verbose_name='LLM Configuration'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='agentconfig',
|
||||||
|
name='system_prompt',
|
||||||
|
field=models.TextField(blank=True, default='', verbose_name='System Prompt'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='agentconfig',
|
||||||
|
name='tool_permissions',
|
||||||
|
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Tool Permissions'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
apps/onboarding/migrations/0003_onboardingflow_structure.py
Normal file
18
apps/onboarding/migrations/0003_onboardingflow_structure.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('onboarding', '0002_alter_agentconfig_llm_config_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='onboardingflow',
|
||||||
|
name='structure',
|
||||||
|
field=models.JSONField(blank=True, default=list, verbose_name='Flow Structure'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
apps/onboarding/migrations/__init__.py
Normal file
0
apps/onboarding/migrations/__init__.py
Normal file
85
apps/onboarding/models.py
Normal file
85
apps/onboarding/models.py
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
from django.db.models import CASCADE, CharField, ForeignKey, JSONField, TextField, Model, DateTimeField, BooleanField
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from apps.accounts.mixins import IdentifierMixin, TimeStampMixin
|
||||||
|
from apps.accounts.models import User, Role, Organization
|
||||||
|
|
||||||
|
class AgentConfig(IdentifierMixin, TimeStampMixin, Model):
|
||||||
|
AGENT_TYPES = [
|
||||||
|
('curriculum', 'Curriculum Agent (CA)'),
|
||||||
|
('knowledge', 'Knowledge Agent (KA)'),
|
||||||
|
('assessment', 'Assessment Agent (AA)'),
|
||||||
|
('monitor', 'Progress Monitor Agent (PMA)'),
|
||||||
|
]
|
||||||
|
|
||||||
|
organization = ForeignKey(Organization, on_delete=CASCADE, related_name='agent_configs', verbose_name=_("Organization"))
|
||||||
|
name = CharField(max_length=255, verbose_name=_("Agent Name"))
|
||||||
|
agent_type = CharField(max_length=40, choices=AGENT_TYPES, verbose_name=_("Agent Type"))
|
||||||
|
llm_config = JSONField(default=dict, blank=True, null=True, verbose_name=_("LLM Configuration"))
|
||||||
|
|
||||||
|
system_prompt = TextField(verbose_name=_("System Prompt"), blank=True, default='')
|
||||||
|
tool_permissions = JSONField(default=list, blank=True, null=True, verbose_name=_("Tool Permissions"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Agent Config')
|
||||||
|
verbose_name_plural = _('Agent Configs')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.get_agent_type_display()})"
|
||||||
|
|
||||||
|
class OnboardingSession(IdentifierMixin, TimeStampMixin, Model):
|
||||||
|
STATUS_CHOICES = [
|
||||||
|
('active', 'Active'),
|
||||||
|
('completed', 'Completed'),
|
||||||
|
('paused', 'Paused'),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = ForeignKey(User, on_delete=CASCADE, related_name='onboarding_sessions', verbose_name=_("User"))
|
||||||
|
role = ForeignKey(Role, on_delete=CASCADE, related_name='onboarding_sessions', verbose_name=_("Target Role"))
|
||||||
|
|
||||||
|
status = CharField(max_length=20, choices=STATUS_CHOICES, default='active', verbose_name=_("Session Status"))
|
||||||
|
state = JSONField(default=dict, blank=True, verbose_name=_("Session State"))
|
||||||
|
|
||||||
|
active_configs = JSONField(default=dict, verbose_name=_("Active Configs"))
|
||||||
|
completed_at = DateTimeField(null=True, blank=True, verbose_name=_("Completed At"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Onboarding Session')
|
||||||
|
verbose_name_plural = _('Onboarding Sessions')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user.email_address} - {self.role.name}"
|
||||||
|
|
||||||
|
class AgentInteractionLog(IdentifierMixin, TimeStampMixin, Model):
|
||||||
|
SENDER_TYPES = [
|
||||||
|
('system', 'System'),
|
||||||
|
('ai', 'AI'),
|
||||||
|
('user', 'User'),
|
||||||
|
('tool', 'Tool Output'),
|
||||||
|
]
|
||||||
|
|
||||||
|
session = ForeignKey(OnboardingSession, on_delete=CASCADE, related_name='logs', verbose_name=_("Session"))
|
||||||
|
agent_config = ForeignKey(AgentConfig, on_delete=CASCADE, null=True, blank=True, verbose_name=_("Agent Config"))
|
||||||
|
sender_type = CharField(max_length=20, choices=SENDER_TYPES, verbose_name=_("Sender Type"))
|
||||||
|
content = TextField(verbose_name=_("Message Content"))
|
||||||
|
tool_call_metadata = JSONField(default=dict, blank=True, verbose_name=_("Tool Call Metadata"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Interaction Log')
|
||||||
|
verbose_name_plural = _('Interaction Logs')
|
||||||
|
ordering = ['created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.sender_type} in {self.session.uuid}"
|
||||||
|
|
||||||
|
class OnboardingFlow(IdentifierMixin, TimeStampMixin, Model):
|
||||||
|
title = CharField(max_length=255, verbose_name=_("Flow Title"))
|
||||||
|
role = ForeignKey(Role, on_delete=CASCADE, related_name='flows', verbose_name=_("Role"))
|
||||||
|
structure = JSONField(default=list, blank=True, verbose_name=_("Flow Structure"))
|
||||||
|
is_active = BooleanField(default=True, verbose_name=_("Is Active"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Onboarding Flow')
|
||||||
|
verbose_name_plural = _('Onboarding Flows')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
6
apps/onboarding/routing.py
Normal file
6
apps/onboarding/routing.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.urls import path
|
||||||
|
from .consumers import OnboardingConsumer
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
path("ws/onboarding/<uuid:session_uuid>/", OnboardingConsumer.as_asgi()),
|
||||||
|
]
|
||||||
69
apps/onboarding/serializers.py
Normal file
69
apps/onboarding/serializers.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
from rest_framework.serializers import CharField, ModelSerializer, SerializerMethodField
|
||||||
|
|
||||||
|
from apps.accounts.serializers import UserSerializer, RoleSerializer, OrganizationSerializer
|
||||||
|
from apps.onboarding.models import AgentConfig, OnboardingSession, AgentInteractionLog, OnboardingFlow
|
||||||
|
|
||||||
|
class AgentConfigSerializer(ModelSerializer):
|
||||||
|
organization = OrganizationSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AgentConfig
|
||||||
|
fields = [
|
||||||
|
'id', 'uuid', 'organization', 'name', 'agent_type',
|
||||||
|
'system_prompt', 'llm_config', 'tool_permissions',
|
||||||
|
'created_at', 'updated_at'
|
||||||
|
]
|
||||||
|
read_only_fields = ['id', 'uuid', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
class AgentInteractionLogSerializer(ModelSerializer):
|
||||||
|
agent_name = CharField(source='agent_config.name', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AgentInteractionLog
|
||||||
|
fields = [
|
||||||
|
'id', 'uuid', 'session', 'agent_config', 'agent_name',
|
||||||
|
'sender_type', 'content', 'tool_call_metadata', 'created_at'
|
||||||
|
]
|
||||||
|
read_only_fields = ['id', 'uuid', 'created_at']
|
||||||
|
|
||||||
|
class OnboardingSessionSerializer(ModelSerializer):
|
||||||
|
user = UserSerializer(read_only=True)
|
||||||
|
role = RoleSerializer(read_only=True)
|
||||||
|
logs = AgentInteractionLogSerializer(many=True, read_only=True)
|
||||||
|
progress_percentage = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OnboardingSession
|
||||||
|
fields = [
|
||||||
|
'id', 'uuid', 'user', 'role', 'status', 'state',
|
||||||
|
'active_configs', 'logs', 'completed_at', 'created_at',
|
||||||
|
'updated_at', 'progress_percentage'
|
||||||
|
]
|
||||||
|
read_only_fields = ['id', 'uuid', 'user', 'completed_at', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
def get_progress_percentage(self, obj: OnboardingSession) -> int:
|
||||||
|
return obj.state.get('progress_percentage', 0)
|
||||||
|
|
||||||
|
class OnboardingFlowSerializer(ModelSerializer):
|
||||||
|
role = RoleSerializer(read_only=True)
|
||||||
|
session_count = SerializerMethodField()
|
||||||
|
pages = SerializerMethodField()
|
||||||
|
description = SerializerMethodField()
|
||||||
|
status = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = OnboardingFlow
|
||||||
|
fields = ['id', 'uuid', 'title', 'role', 'is_active', 'status', 'description', 'pages', 'session_count', 'created_at']
|
||||||
|
read_only_fields = ['id', 'uuid', 'created_at']
|
||||||
|
|
||||||
|
def get_session_count(self, obj: OnboardingFlow) -> int:
|
||||||
|
return obj.role.onboarding_sessions.count()
|
||||||
|
|
||||||
|
def get_pages(self, obj: OnboardingFlow):
|
||||||
|
return obj.structure or []
|
||||||
|
|
||||||
|
def get_description(self, obj: OnboardingFlow) -> str:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_status(self, obj: OnboardingFlow) -> str:
|
||||||
|
return 'published' if obj.is_active else 'archived'
|
||||||
159
apps/onboarding/viewsets.py
Normal file
159
apps/onboarding/viewsets.py
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN, HTTP_201_CREATED, HTTP_200_OK
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
|
from apps.onboarding.models import AgentConfig, OnboardingSession, AgentInteractionLog, OnboardingFlow
|
||||||
|
from apps.onboarding.serializers import (
|
||||||
|
AgentConfigSerializer,
|
||||||
|
OnboardingSessionSerializer,
|
||||||
|
AgentInteractionLogSerializer,
|
||||||
|
OnboardingFlowSerializer
|
||||||
|
)
|
||||||
|
|
||||||
|
class OnboardingFlowViewSet(ModelViewSet):
|
||||||
|
queryset = OnboardingFlow.objects.all()
|
||||||
|
serializer_class = OnboardingFlowSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
return OnboardingFlow.objects.filter(
|
||||||
|
Q(role__organization__owner=user) |
|
||||||
|
Q(role__organization__members=user)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
flow = self.get_object()
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
OnboardingSession.objects.filter(role=flow.role).delete()
|
||||||
|
self.perform_destroy(flow)
|
||||||
|
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path='start-session')
|
||||||
|
def start_session(self, request, uuid=None):
|
||||||
|
flow = self.get_object()
|
||||||
|
|
||||||
|
session, created = OnboardingSession.objects.get_or_create(
|
||||||
|
user=request.user,
|
||||||
|
role=flow.role,
|
||||||
|
defaults={
|
||||||
|
'status': 'active',
|
||||||
|
'state': {
|
||||||
|
'progress': 0,
|
||||||
|
'current_step': 'intro',
|
||||||
|
'flow_uuid': str(flow.uuid),
|
||||||
|
},
|
||||||
|
'active_configs': {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
state = session.state or {}
|
||||||
|
state['flow_uuid'] = str(flow.uuid)
|
||||||
|
session.state = state
|
||||||
|
session.save(update_fields=['state', 'updated_at'])
|
||||||
|
|
||||||
|
serializer = OnboardingSessionSerializer(session)
|
||||||
|
return Response(serializer.data, status=HTTP_201_CREATED if created else HTTP_200_OK)
|
||||||
|
|
||||||
|
class AgentConfigViewSet(ModelViewSet):
|
||||||
|
queryset = AgentConfig.objects.all()
|
||||||
|
serializer_class = AgentConfigSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return AgentConfig.objects.filter(organization__members=self.request.user).distinct()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
if not self.request.user.is_manager:
|
||||||
|
return Response({'error': 'Forbidden'}, status=HTTP_403_FORBIDDEN)
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
class OnboardingSessionViewSet(ModelViewSet):
|
||||||
|
queryset = OnboardingSession.objects.all()
|
||||||
|
serializer_class = OnboardingSessionSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user
|
||||||
|
if user.is_manager:
|
||||||
|
return OnboardingSession.objects.filter(role__organization__members=user).distinct()
|
||||||
|
return OnboardingSession.objects.filter(user=user)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path='interact')
|
||||||
|
def interact(self, request, uuid=None):
|
||||||
|
session = self.get_object()
|
||||||
|
user_message = request.data.get('message')
|
||||||
|
page_uuid = request.data.get('page_uuid')
|
||||||
|
responses = request.data.get('responses')
|
||||||
|
|
||||||
|
if not user_message and not page_uuid:
|
||||||
|
return Response({'error': 'Message or page_uuid is required'}, status=HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if isinstance(responses, dict):
|
||||||
|
state = session.state or {}
|
||||||
|
stored_responses = state.get('responses', {})
|
||||||
|
if not isinstance(stored_responses, dict):
|
||||||
|
stored_responses = {}
|
||||||
|
|
||||||
|
if page_uuid:
|
||||||
|
stored_responses[str(page_uuid)] = responses
|
||||||
|
else:
|
||||||
|
stored_responses.update(responses)
|
||||||
|
|
||||||
|
state['responses'] = stored_responses
|
||||||
|
if page_uuid:
|
||||||
|
state['last_page_uuid'] = str(page_uuid)
|
||||||
|
session.state = state
|
||||||
|
session.save(update_fields=['state', 'updated_at'])
|
||||||
|
|
||||||
|
AgentInteractionLog.objects.create(
|
||||||
|
session=session,
|
||||||
|
sender_type='user',
|
||||||
|
content=user_message or f'Submitted onboarding responses for page {page_uuid or "unknown"}',
|
||||||
|
tool_call_metadata={'page_uuid': page_uuid, 'has_responses': isinstance(responses, dict)}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'status': 'received',
|
||||||
|
'session_state': session.state
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['get'], url_path='history')
|
||||||
|
def history(self, request, uuid=None):
|
||||||
|
session = self.get_object()
|
||||||
|
logs = session.logs.all().order_by('created_at')
|
||||||
|
serializer = AgentInteractionLogSerializer(logs, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path='complete')
|
||||||
|
def complete(self, request, uuid=None):
|
||||||
|
session = self.get_object()
|
||||||
|
session.status = 'completed'
|
||||||
|
session.completed_at = timezone.now()
|
||||||
|
session.save()
|
||||||
|
return Response({'message': 'Session marked as completed'})
|
||||||
|
|
||||||
|
class AgentInteractionLogViewSet(ReadOnlyModelViewSet):
|
||||||
|
queryset = AgentInteractionLog.objects.all()
|
||||||
|
serializer_class = AgentInteractionLogSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
lookup_field = 'uuid'
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return AgentInteractionLog.objects.filter(
|
||||||
|
Q(session__user=self.request.user) |
|
||||||
|
Q(session__role__organization__owner=self.request.user)
|
||||||
|
).distinct()
|
||||||
19
compose/dev/celery/Dockerfile
Normal file
19
compose/dev/celery/Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
FROM python:3.12-bookworm
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV VIRTUAL_ENV=/venv \
|
||||||
|
PATH=/venv/bin:$PATH
|
||||||
|
|
||||||
|
RUN python -m venv /venv
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements/django.txt .
|
||||||
|
RUN pip install --no-cache-dir --requirement django.txt
|
||||||
|
|
||||||
|
CMD ["celery", "-A", "config", "worker", "-l", "info"]
|
||||||
23
compose/dev/django/Dockerfile
Normal file
23
compose/dev/django/Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
FROM python:3.12-bookworm
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
wait-for-it \
|
||||||
|
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV VIRTUAL_ENV=/venv \
|
||||||
|
PATH=/venv/bin:$PATH
|
||||||
|
|
||||||
|
RUN python -m venv /venv
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements/django.txt .
|
||||||
|
RUN pip install --no-cache-dir --requirement django.txt
|
||||||
|
|
||||||
|
COPY ./compose/dev/django/start /start
|
||||||
|
RUN sed -i 's/\r$//g' /start && chmod +x /start
|
||||||
|
|
||||||
|
CMD ["/start"]
|
||||||
27
compose/dev/django/start
Normal file
27
compose/dev/django/start
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o pipefail
|
||||||
|
set -o nounset
|
||||||
|
|
||||||
|
DB_HOST="${POSTGRES_HOST}"
|
||||||
|
DB_PORT="${POSTGRES_PORT}"
|
||||||
|
|
||||||
|
echo "Waiting for database at ${DB_HOST}:${DB_PORT}..."
|
||||||
|
wait-for-it ${DB_HOST}:${DB_PORT} --timeout=30 --strict || {
|
||||||
|
echo "Timed out waiting for database" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Database is available, continuing startup..."
|
||||||
|
|
||||||
|
python manage.py makemigrations
|
||||||
|
python manage.py migrate --noinput
|
||||||
|
|
||||||
|
for fixture in /app/data/*.json; do
|
||||||
|
echo "Loading fixture: $fixture"
|
||||||
|
python manage.py loaddata "$fixture"
|
||||||
|
done
|
||||||
|
|
||||||
|
python manage.py collectstatic --noinput
|
||||||
|
exec python manage.py runserver 0.0.0:8000
|
||||||
118
compose/dev/docker-compose.yml
Normal file
118
compose/dev/docker-compose.yml
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
services:
|
||||||
|
fyp-django-dev:
|
||||||
|
container_name: fyp-django-dev
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: compose/dev/django/Dockerfile
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
volumes:
|
||||||
|
- ../../:/app
|
||||||
|
ports:
|
||||||
|
- '0.0.0.0:8000:8000'
|
||||||
|
depends_on:
|
||||||
|
fyp-postgres-dev:
|
||||||
|
condition: service_healthy
|
||||||
|
fyp-node-dev:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
|
fyp-node-dev:
|
||||||
|
container_name: fyp-node-dev
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: compose/dev/node/Dockerfile
|
||||||
|
environment:
|
||||||
|
NODE_ENV: development
|
||||||
|
CHOKIDAR_USEPOLLING: 'true'
|
||||||
|
stdin_open: true
|
||||||
|
volumes:
|
||||||
|
- ../../site:/app:delegated
|
||||||
|
- /app/node_modules
|
||||||
|
ports:
|
||||||
|
- '0.0.0.0:5173:5173'
|
||||||
|
|
||||||
|
fyp-postgres-dev:
|
||||||
|
container_name: fyp-postgres-dev
|
||||||
|
image: pgvector/pgvector:pg15
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
environment:
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
volumes:
|
||||||
|
- fyp_postgres_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- '0.0.0.0:5432:5432'
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -p 5432 -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
fyp-redis-dev:
|
||||||
|
container_name: fyp-redis-dev
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- '0.0.0.0:6379:6379'
|
||||||
|
volumes:
|
||||||
|
- fyp_redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
fyp-celery-dev:
|
||||||
|
container_name: fyp-celery-dev
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: compose/dev/celery/Dockerfile
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
volumes:
|
||||||
|
- ../../:/app
|
||||||
|
depends_on:
|
||||||
|
fyp-redis-dev:
|
||||||
|
condition: service_healthy
|
||||||
|
fyp-postgres-dev:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
|
||||||
|
fyp-inference-dev:
|
||||||
|
container_name: fyp-inference-dev
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: compose/dev/inference/Dockerfile
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
volumes:
|
||||||
|
- ../../:/app
|
||||||
|
- ../../models:/app/models
|
||||||
|
- hf_cache:/root/.cache/huggingface
|
||||||
|
deploy:
|
||||||
|
mode: replicated
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
environment:
|
||||||
|
- NVIDIA_VISIBLE_DEVICES=all
|
||||||
|
- WATCHFILES_FORCE_POLLING=true
|
||||||
|
- PYTHONPATH=/app
|
||||||
|
- HF_HOME=/root/.cache/huggingface
|
||||||
|
- HF_HUB_OFFLINE=1
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:8001:8001"
|
||||||
|
depends_on:
|
||||||
|
fyp-redis-dev:
|
||||||
|
condition: service_healthy
|
||||||
|
fyp-postgres-dev:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
fyp_postgres_data:
|
||||||
|
fyp_redis_data:
|
||||||
|
hf_cache:
|
||||||
35
compose/dev/inference/Dockerfile
Normal file
35
compose/dev/inference/Dockerfile
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
FROM nvidia/cuda:12.4.1-devel-ubuntu22.04 AS builder
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y python3.10 python3-pip python3-dev cmake git
|
||||||
|
|
||||||
|
COPY requirements/inference.txt .
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip setuptools wheel
|
||||||
|
RUN pip install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
|
||||||
|
|
||||||
|
ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64/stubs:$LD_LIBRARY_PATH
|
||||||
|
|
||||||
|
ENV CMAKE_ARGS="-DGGML_CUDA=on -DLLAVA_BUILD=off"
|
||||||
|
ENV FORCE_CMAKE=1
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir llama-cpp-python
|
||||||
|
RUN pip install --no-cache-dir -r inference.txt
|
||||||
|
|
||||||
|
FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y python3.10 python3-pip && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
ln -sf /usr/bin/python3 /usr/bin/python
|
||||||
|
|
||||||
|
COPY --from=builder /usr/local/lib/python3.10/dist-packages /usr/local/lib/python3.10/dist-packages
|
||||||
|
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV PYTHONPATH=/app
|
||||||
|
EXPOSE 8001
|
||||||
|
|
||||||
|
CMD ["python", "-m", "uvicorn", "gpu_server:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||||
15
compose/dev/node/Dockerfile
Normal file
15
compose/dev/node/Dockerfile
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
FROM node:22-bullseye
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY site/package*.json ./
|
||||||
|
RUN npm ci && npm cache clean --force
|
||||||
|
|
||||||
|
COPY site/src ./src
|
||||||
|
COPY site/index.html .
|
||||||
|
COPY site/vite.config.* .
|
||||||
|
COPY site/tsconfig.* .
|
||||||
|
|
||||||
|
EXPOSE 5173
|
||||||
|
|
||||||
|
CMD ["npm", "run", "devwatch"]
|
||||||
27
compose/prod/celery/Dockerfile
Normal file
27
compose/prod/celery/Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
FROM python:3.12.0-slim
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="Dynavera Celery Worker"
|
||||||
|
LABEL org.opencontainers.image.source="https://git.cs.bham.ac.uk/projects-2025-26/vxn217"
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements/django.txt .
|
||||||
|
RUN pip install --no-cache-dir -r django.txt
|
||||||
|
|
||||||
|
COPY manage.py manage.py
|
||||||
|
COPY config config
|
||||||
|
COPY apps apps
|
||||||
|
COPY data data
|
||||||
|
COPY mcp_agent mcp_agent
|
||||||
|
|
||||||
|
RUN mkdir -p /app/static
|
||||||
|
|
||||||
|
CMD ["celery", "-A", "config.celery", "worker", "--loglevel=info"]
|
||||||
47
compose/prod/django/Dockerfile
Normal file
47
compose/prod/django/Dockerfile
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
FROM node:22-alpine AS node
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY site/package*.json ./
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY site/ ./
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM python:3.12.0-slim AS python
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="Dynavera - An Agentic Approach to Role-Specific Trainers"
|
||||||
|
LABEL org.opencontainers.image.source="https://git.cs.bham.ac.uk/projects-2025-26/vxn217"
|
||||||
|
LABEL org.opencontainers.image.description="Dynavera (Final Year Project)"
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
wait-for-it \
|
||||||
|
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements/django.txt .
|
||||||
|
RUN pip install --no-cache-dir -r django.txt
|
||||||
|
|
||||||
|
COPY manage.py manage.py
|
||||||
|
COPY config config
|
||||||
|
COPY apps apps
|
||||||
|
COPY data data
|
||||||
|
COPY mcp_agent mcp_agent
|
||||||
|
|
||||||
|
COPY --from=node /app/build ./build
|
||||||
|
|
||||||
|
RUN mkdir -p /app/static
|
||||||
|
|
||||||
|
COPY ./compose/prod/django/start /start
|
||||||
|
RUN sed -i 's/\r$//g' /start && chmod +x /start
|
||||||
|
|
||||||
|
ENTRYPOINT ["/start"]
|
||||||
26
compose/prod/docker-compose.inference.yml
Normal file
26
compose/prod/docker-compose.inference.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
services:
|
||||||
|
fyp-inference-prod:
|
||||||
|
container_name: fyp-inference-prod
|
||||||
|
build:
|
||||||
|
context: ../../
|
||||||
|
dockerfile: compose/dev/inference/Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
mode: replicated
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
environment:
|
||||||
|
- INFERENCE_HTTP_HOST=0.0.0.0
|
||||||
|
- INFERENCE_HTTP_PORT=8001
|
||||||
|
- NVIDIA_VISIBLE_DEVICES=all
|
||||||
|
ports:
|
||||||
|
- '0.0.0.0:58001:8001'
|
||||||
|
volumes:
|
||||||
|
- ../../:/app
|
||||||
122
compose/prod/docker-compose.yml
Normal file
122
compose/prod/docker-compose.yml
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
services:
|
||||||
|
fyp-django-prod:
|
||||||
|
container_name: fyp-django-prod
|
||||||
|
image: "${FYP_DJANGO_IMAGE}"
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.fyp-web.rule=Host(`${DJANGO_DOMAIN_NAME}`)"
|
||||||
|
- "traefik.http.routers.fyp-web.entrypoints=${DJANGO_ENTRYPOINT}"
|
||||||
|
- "traefik.http.routers.fyp-web.tls.certresolver=${CERTRESOLVER}"
|
||||||
|
- "traefik.http.routers.fyp-web.tls=true"
|
||||||
|
- "traefik.http.services.fyp-web.loadbalancer.server.port=${DJANGO_PORT}"
|
||||||
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
|
- "com.centurylinklabs.watchtower.scope=fyp"
|
||||||
|
volumes:
|
||||||
|
- ../../static:/app/static
|
||||||
|
- ../../media:/app/media
|
||||||
|
depends_on:
|
||||||
|
fyp-postgres-prod:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- fyp-network
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
fyp-postgres-prod:
|
||||||
|
container_name: fyp-postgres-prod
|
||||||
|
image: pgvector/pgvector:pg15
|
||||||
|
hostname: fyp-postgres-prod
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
environment:
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
volumes:
|
||||||
|
- fyp_postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -p 5432 -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- fyp-network
|
||||||
|
|
||||||
|
fyp-redis-prod:
|
||||||
|
container_name: fyp-redis-prod
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- fyp_redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- fyp-network
|
||||||
|
|
||||||
|
fyp-celery-prod:
|
||||||
|
container_name: fyp-celery-prod
|
||||||
|
image: "${FYP_CELERY_IMAGE}"
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
volumes:
|
||||||
|
- ../../:/app
|
||||||
|
- ../../static:/app/static
|
||||||
|
- ../../media:/app/media
|
||||||
|
depends_on:
|
||||||
|
fyp-redis-prod:
|
||||||
|
condition: service_healthy
|
||||||
|
fyp-postgres-prod:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- fyp-network
|
||||||
|
|
||||||
|
fyp-watchtower-prod:
|
||||||
|
container_name: fyp-watchtower-prod
|
||||||
|
image: containrrr/watchtower
|
||||||
|
command:
|
||||||
|
- "--scope=fyp"
|
||||||
|
- "--label-enable"
|
||||||
|
- "--interval"
|
||||||
|
- "30"
|
||||||
|
- "--rolling-restart"
|
||||||
|
environment:
|
||||||
|
- WATCHTOWER_CLEANUP=true
|
||||||
|
- REPO_USER=${GITLAB_USER}
|
||||||
|
- REPO_PASS=${GITLAB_PASS}
|
||||||
|
volumes:
|
||||||
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
|
|
||||||
|
fyp-runner-prod:
|
||||||
|
container_name: fyp-runner-prod
|
||||||
|
image: gitlab/gitlab-runner:${GITLAB_RUNNER_IMAGE_TAG}
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- CI_SERVER_URL=${GITLAB_SERVER_URL}
|
||||||
|
- REGISTRATION_TOKEN=${GITLAB_RUNNER_REGISTRATION_TOKEN}
|
||||||
|
- RUNNER_EXECUTOR=docker
|
||||||
|
- RUNNER_RUN_UNTAGGED=true
|
||||||
|
- RUNNER_TAG_LIST=
|
||||||
|
- DOCKER_TLS_CERTDIR=
|
||||||
|
- DOCKER_IMAGE=${GITLAB_RUNNER_DOCKER_IMAGE}
|
||||||
|
volumes:
|
||||||
|
- gitlab-runner-config:/etc/gitlab-runner
|
||||||
|
- gitlab-machine-config:/root/.docker/machine
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
command:
|
||||||
|
- run
|
||||||
|
- "--working-directory=/home/gitlab-runner"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
fyp_postgres_data:
|
||||||
|
fyp_redis_data:
|
||||||
|
gitlab-runner-config:
|
||||||
|
gitlab-machine-config:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
fyp-network:
|
||||||
|
driver: bridge
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
3
config/__init__.py
Normal file
3
config/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .celery import app as celery_app
|
||||||
|
|
||||||
|
__all__ = ('celery_app',)
|
||||||
18
config/api.py
Normal file
18
config/api.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from apps.accounts.viewsets import UserViewSet, OrganizationViewSet
|
||||||
|
from apps.knowledge.viewsets import TrainingFileViewSet, RoleRagDocumentViewSet
|
||||||
|
from apps.onboarding.viewsets import AgentConfigViewSet, OnboardingFlowViewSet, OnboardingSessionViewSet, AgentInteractionLogViewSet
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
|
||||||
|
router.register(r'user', UserViewSet)
|
||||||
|
router.register(r'organization', OrganizationViewSet)
|
||||||
|
router.register(r'training-file', TrainingFileViewSet)
|
||||||
|
router.register(r'role-rag-document', RoleRagDocumentViewSet)
|
||||||
|
router.register(r'agent-config', AgentConfigViewSet)
|
||||||
|
router.register(r'onboarding-flow', OnboardingFlowViewSet)
|
||||||
|
router.register(r'onboarding-session', OnboardingSessionViewSet)
|
||||||
|
router.register(r'agent-interaction-log', AgentInteractionLogViewSet)
|
||||||
|
|
||||||
|
urlpatterns = router.urls
|
||||||
21
config/asgi.py
Normal file
21
config/asgi.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
from channels.auth import AuthMiddlewareStack
|
||||||
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
|
from channels.security.websocket import AllowedHostsOriginValidator
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
django_asgi_app = get_asgi_application()
|
||||||
|
|
||||||
|
from apps.onboarding.routing import websocket_urlpatterns
|
||||||
|
|
||||||
|
application = ProtocolTypeRouter({
|
||||||
|
"http": django_asgi_app,
|
||||||
|
"websocket": AllowedHostsOriginValidator(
|
||||||
|
AuthMiddlewareStack(
|
||||||
|
URLRouter(websocket_urlpatterns)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
8
config/celery.py
Normal file
8
config/celery.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from celery import Celery
|
||||||
|
import os
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
app = Celery('config')
|
||||||
|
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||||
|
app.autodiscover_tasks()
|
||||||
210
config/settings.py
Normal file
210
config/settings.py
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
"""
|
||||||
|
Django settings will use prefix of DJANGO_ for environment variables.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
load_dotenv(dotenv_path = BASE_DIR / '.env')
|
||||||
|
|
||||||
|
FRONT_DIR = os.getenv('DJANGO_FRONT_DIR', BASE_DIR / 'front')
|
||||||
|
MODEL_DIR = os.getenv('DJANGO_MODEL_DIR', BASE_DIR / 'model')
|
||||||
|
|
||||||
|
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
|
||||||
|
DEBUG = str(os.getenv('DJANGO_DEBUG')).lower() in ('1', 'true', 'yes', 'on')
|
||||||
|
|
||||||
|
DOMAIN_NAME = os.getenv('DJANGO_DOMAIN_NAME', 'localhost')
|
||||||
|
ALLOWED_HOSTS = [stripped_host for host in os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',') if (stripped_host:=host.strip())]
|
||||||
|
|
||||||
|
PARENT_NAME = Path(__file__).resolve().parent.name
|
||||||
|
|
||||||
|
DJANGO_CELERY_BROKER_URL = os.getenv('DJANGO_CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
||||||
|
|
||||||
|
INFERENCE_HOST = os.getenv('INFERENCE_HOST', 'localhost')
|
||||||
|
INFERENCE_PORT = os.getenv('INFERENCE_PORT', '8001')
|
||||||
|
INFERENCE_URL = f"http://{INFERENCE_HOST}:{INFERENCE_PORT}"
|
||||||
|
INFERENCE_INGEST_TIMEOUT = float(os.getenv('INFERENCE_INGEST_TIMEOUT', '600'))
|
||||||
|
|
||||||
|
STATIC_URL = os.getenv('DJANGO_STATIC_URL', '/static/')
|
||||||
|
MEDIA_URL = os.getenv('DJANGO_MEDIA_URL', '/media/')
|
||||||
|
STATIC_ROOT = os.getenv('DJANGO_STATIC_ROOT', BASE_DIR / 'static')
|
||||||
|
MEDIA_ROOT = os.getenv('DJANGO_MEDIA_ROOT', BASE_DIR / 'media')
|
||||||
|
|
||||||
|
DB_ENGINE = os.getenv('DJANGO_DB_ENGINE', 'django.db.backends.sqlite3')
|
||||||
|
DB_NAME = os.getenv('POSTGRES_DB', BASE_DIR / 'db.sqlite3')
|
||||||
|
DB_USER = os.getenv('POSTGRES_USER')
|
||||||
|
DB_PASSWORD = os.getenv('POSTGRES_PASSWORD')
|
||||||
|
DB_HOST = os.getenv('POSTGRES_HOST')
|
||||||
|
DB_PORT = os.getenv('POSTGRES_PORT', 5432)
|
||||||
|
|
||||||
|
if any(arg.startswith('test') for arg in sys.argv):
|
||||||
|
DB_ENGINE = 'django.db.backends.sqlite3'
|
||||||
|
DB_NAME = ':memory:'
|
||||||
|
DB_USER = None
|
||||||
|
DB_PASSWORD = None
|
||||||
|
DB_HOST = None
|
||||||
|
DB_PORT = None
|
||||||
|
|
||||||
|
OVERRIDE_APPS = [
|
||||||
|
|
||||||
|
|
||||||
|
'daphne',
|
||||||
|
]
|
||||||
|
DJANGO_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
]
|
||||||
|
THIRD_PARTY_APPS = [
|
||||||
|
'rest_framework',
|
||||||
|
'channels',
|
||||||
|
'django_celery_results',
|
||||||
|
'corsheaders',
|
||||||
|
]
|
||||||
|
LOCAL_APPS = [
|
||||||
|
'apps.accounts',
|
||||||
|
'apps.onboarding',
|
||||||
|
'apps.knowledge',
|
||||||
|
]
|
||||||
|
INSTALLED_APPS = OVERRIDE_APPS + DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'accounts.User'
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = f'{PARENT_NAME}.urls'
|
||||||
|
WSGI_APPLICATION = f'{PARENT_NAME}.wsgi.application'
|
||||||
|
ASGI_APPLICATION = f'{PARENT_NAME}.asgi.application'
|
||||||
|
|
||||||
|
CHANNEL_LAYERS = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||||
|
'CONFIG': {
|
||||||
|
'hosts': [DJANGO_CELERY_BROKER_URL],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': DB_ENGINE,
|
||||||
|
'NAME': DB_NAME,
|
||||||
|
} if DB_ENGINE == 'django.db.backends.sqlite3' else {
|
||||||
|
'ENGINE': DB_ENGINE,
|
||||||
|
'NAME': DB_NAME,
|
||||||
|
'USER': DB_USER,
|
||||||
|
'PASSWORD': DB_PASSWORD,
|
||||||
|
'HOST': DB_HOST,
|
||||||
|
'PORT': DB_PORT,
|
||||||
|
'CONN_MAX_AGE': 600,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-uk'
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
|
],
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
'rest_framework.permissions.AllowAny',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
CELERY_BROKER_URL = DJANGO_CELERY_BROKER_URL
|
||||||
|
CELERY_RESULT_BACKEND = 'django-db'
|
||||||
|
CELERY_CACHE_BACKEND = 'django-cache'
|
||||||
|
CELERY_ACCEPT_CONTENT = ['json']
|
||||||
|
CELERY_TASK_SERIALIZER = 'json'
|
||||||
|
CELERY_RESULT_SERIALIZER = 'json'
|
||||||
|
CELERY_TIMEZONE = 'UTC'
|
||||||
|
CELERY_TASK_TRACK_STARTED = True
|
||||||
|
CELERY_TASK_TIME_LIMIT = 30 * 60
|
||||||
|
|
||||||
|
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
f'http://{DOMAIN_NAME}',
|
||||||
|
f'https://{DOMAIN_NAME}',
|
||||||
|
]
|
||||||
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
|
f'http://{DOMAIN_NAME}',
|
||||||
|
f'https://{DOMAIN_NAME}',
|
||||||
|
]
|
||||||
|
CSRF_COOKIE_HTTPONLY = False
|
||||||
|
CSRF_COOKIE_SECURE = not DEBUG
|
||||||
|
CSRF_COOKIE_SAMESITE = 'Lax'
|
||||||
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
SESSION_COOKIE_SECURE = not DEBUG
|
||||||
|
SESSION_COOKIE_AGE = 1209600
|
||||||
|
SESSION_SAVE_EVERY_REQUEST = True
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
CORS_ALLOWED_ORIGINS.append(f'http://{DOMAIN_NAME}:5173')
|
||||||
|
CORS_ALLOWED_ORIGINS.append(f'http://{DOMAIN_NAME}:8000')
|
||||||
|
CSRF_TRUSTED_ORIGINS.append(f'http://{DOMAIN_NAME}:5173')
|
||||||
|
CSRF_TRUSTED_ORIGINS.append(f'http://{DOMAIN_NAME}:8000')
|
||||||
14
config/urls.py
Normal file
14
config/urls.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include, re_path
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
from .views import serve_frontend
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('api/', include('config.api')),
|
||||||
|
re_path(r'^(?!static/|media/)(?P<path>.*)$', serve_frontend, {'document_root': settings.FRONT_DIR}),
|
||||||
|
*static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
|
||||||
|
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
|
||||||
|
]
|
||||||
16
config/views.py
Normal file
16
config/views.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import posixpath
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.utils._os import safe_join
|
||||||
|
from django.views.static import serve as static_serve
|
||||||
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
|
||||||
|
@ensure_csrf_cookie
|
||||||
|
def serve_frontend(request, path, document_root = None):
|
||||||
|
print(f"Serving path: {path} from {document_root}")
|
||||||
|
path = posixpath.normpath(path).lstrip("/")
|
||||||
|
fullpath = Path(safe_join(document_root, path))
|
||||||
|
if fullpath.is_file():
|
||||||
|
return static_serve(request, path, document_root)
|
||||||
|
else:
|
||||||
|
return static_serve(request, "index.html", document_root)
|
||||||
5
config/wsgi.py
Normal file
5
config/wsgi.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import os
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
application = get_wsgi_application()
|
||||||
254
data/1_users.json
Normal file
254
data/1_users.json
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "d8c80f82-b668-4035-a8ac-dfc8f0985f5d",
|
||||||
|
"created_at": "2026-02-24T19:35:49.084Z",
|
||||||
|
"updated_at": "2026-02-24T19:35:49.084Z",
|
||||||
|
"email_address": "admin@example.com",
|
||||||
|
"first_name": "Ad",
|
||||||
|
"last_name": "Min",
|
||||||
|
"date_of_birth": "2001-01-01",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": true,
|
||||||
|
"is_manager": true,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$SSYxNyuMktzVMLlQteB95g$KCjU5+OHkufDiWIZAahYP7JFPGegXCwv6NsEUXnX+gY=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "4039d920-3cbf-4a49-9be0-196dda6f33a1",
|
||||||
|
"created_at": "2026-02-24T19:35:11.034Z",
|
||||||
|
"updated_at": "2026-02-24T19:35:11.034Z",
|
||||||
|
"email_address": "haleisaac@example.com",
|
||||||
|
"first_name": "Hale",
|
||||||
|
"last_name": "Isaac",
|
||||||
|
"date_of_birth": "1983-10-12",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": true,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "e29c8b74-29c3-4d7b-9f9c-7c89d21a9e33",
|
||||||
|
"created_at": "2026-02-25T10:00:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:00:00.000Z",
|
||||||
|
"email_address": "sarah.chen@example.com",
|
||||||
|
"first_name": "Sarah",
|
||||||
|
"last_name": "Chen",
|
||||||
|
"date_of_birth": "1995-04-14",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": false,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "bc731298-f23a-4932-8412-192837465ab1",
|
||||||
|
"created_at": "2026-02-25T10:05:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:05:00.000Z",
|
||||||
|
"email_address": "marcus.v@example.com",
|
||||||
|
"first_name": "Marcus",
|
||||||
|
"last_name": "Vance",
|
||||||
|
"date_of_birth": "1988-11-22",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": false,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "5f9e1c2d-8a4b-47e6-b3a1-9c8d7e6f5a4b",
|
||||||
|
"created_at": "2026-02-25T10:10:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:10:00.000Z",
|
||||||
|
"email_address": "elara.smith@example.com",
|
||||||
|
"first_name": "Elara",
|
||||||
|
"last_name": "Smith",
|
||||||
|
"date_of_birth": "2002-06-30",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": true,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "7a8b9c0d-1e2f-4a5b-bcde-f0123456789a",
|
||||||
|
"created_at": "2026-02-25T10:15:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:15:00.000Z",
|
||||||
|
"email_address": "j.thompson@example.com",
|
||||||
|
"first_name": "James",
|
||||||
|
"last_name": "Thompson",
|
||||||
|
"date_of_birth": "1990-01-15",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": false,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "11223344-5566-7788-99aa-bbccddeeff00",
|
||||||
|
"created_at": "2026-02-25T10:20:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:20:00.000Z",
|
||||||
|
"email_address": "priya.p@example.com",
|
||||||
|
"first_name": "Priya",
|
||||||
|
"last_name": "Patel",
|
||||||
|
"date_of_birth": "1997-08-05",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": false,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "ffeeddcc-bbaa-9988-7766-554433221100",
|
||||||
|
"created_at": "2026-02-25T10:25:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:25:00.000Z",
|
||||||
|
"email_address": "lewis.hamil@example.com",
|
||||||
|
"first_name": "Lewis",
|
||||||
|
"last_name": "Hamilton",
|
||||||
|
"date_of_birth": "1985-01-07",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": false,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 9,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "abcdef01-2345-6789-abcd-ef0123456789",
|
||||||
|
"created_at": "2026-02-25T10:30:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:30:00.000Z",
|
||||||
|
"email_address": "nina.simone@example.com",
|
||||||
|
"first_name": "Nina",
|
||||||
|
"last_name": "Simone",
|
||||||
|
"date_of_birth": "2000-12-12",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": true,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 10,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "99887766-5544-3322-1100-abcdefabcdef",
|
||||||
|
"created_at": "2026-02-25T10:35:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:35:00.000Z",
|
||||||
|
"email_address": "oscar.w@example.com",
|
||||||
|
"first_name": "Oscar",
|
||||||
|
"last_name": "Wilder",
|
||||||
|
"date_of_birth": "1992-03-22",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": false,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 11,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "44332211-8877-6655-4433-221100998877",
|
||||||
|
"created_at": "2026-02-25T10:40:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:40:00.000Z",
|
||||||
|
"email_address": "claire.d@example.com",
|
||||||
|
"first_name": "Claire",
|
||||||
|
"last_name": "Danes",
|
||||||
|
"date_of_birth": "1994-05-19",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": false,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.user",
|
||||||
|
"pk": 12,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$1200000$k8zzmUMrpF0onsHIygd72k$7Ax/w0HZDaRw48pului25uKeM+115wXj6H3MLjvRGUE=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": false,
|
||||||
|
"uuid": "00aa11bb-22cc-33dd-44ee-55ff66aa77bb",
|
||||||
|
"created_at": "2026-02-25T10:45:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T10:45:00.000Z",
|
||||||
|
"email_address": "dev.tester@example.com",
|
||||||
|
"first_name": "Dev",
|
||||||
|
"last_name": "Tester",
|
||||||
|
"date_of_birth": "1999-09-09",
|
||||||
|
"is_active": true,
|
||||||
|
"is_staff": false,
|
||||||
|
"is_manager": true,
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
41
data/2_organizations.json
Normal file
41
data/2_organizations.json
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "accounts.organization",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "314324a0-9fda-4579-ad90-96123b187f97",
|
||||||
|
"created_at": "2026-02-24T23:03:53.518Z",
|
||||||
|
"updated_at": "2026-02-25T20:30:00.000Z",
|
||||||
|
"name": "University of Birmingham",
|
||||||
|
"description": "The University of Birmingham is a public research university in Birmingham, England. It is a founding member of the Russell Group and the international network Universitas 21.",
|
||||||
|
"owner": 2,
|
||||||
|
"members": [2, 3, 4, 5, 6, 7]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.organization",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "71c14993-e1a6-448a-af2c-17f50043f545",
|
||||||
|
"created_at": "2026-02-24T23:04:55.810Z",
|
||||||
|
"updated_at": "2026-02-25T20:30:00.000Z",
|
||||||
|
"name": "Example Organization",
|
||||||
|
"description": "This is an example fictional organization that has many roles, used for testing administrative workflows.",
|
||||||
|
"owner": 1,
|
||||||
|
"members": [1, 2, 8, 9, 12]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.organization",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||||
|
"created_at": "2026-02-25T20:30:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T20:30:00.000Z",
|
||||||
|
"name": "Silicon Canal Tech Hub",
|
||||||
|
"description": "A collective of technology innovators and fintech enthusiasts based in the West Midlands, focusing on financial literacy and stock market simulation.",
|
||||||
|
"owner": 12,
|
||||||
|
"members": [12, 3, 5, 9, 10, 11]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
106
data/3_roles.json
Normal file
106
data/3_roles.json
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "accounts.role",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "51a671c3-e680-4d81-af39-194939313b93",
|
||||||
|
"created_at": "2026-02-25T12:51:23.873Z",
|
||||||
|
"updated_at": "2026-02-25T12:51:23.873Z",
|
||||||
|
"name": "UX Developer",
|
||||||
|
"description": "A hybrid professional bridging design and front-end engineering, responsible for both designing user-centric interfaces and coding them.",
|
||||||
|
"organization": 2,
|
||||||
|
"members": [1, 4]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.role",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "72b8d1a4-f791-4e92-bc40-205040424c04",
|
||||||
|
"created_at": "2026-02-25T13:00:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T13:00:00.000Z",
|
||||||
|
"name": "fNIRS Specialist",
|
||||||
|
"description": "Functional Near-Infrared Spectroscopy Specialist responsible for neuroimaging data collection and analyzing cortical hemodynamic responses.",
|
||||||
|
"organization": 1,
|
||||||
|
"members": [3, 5]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.role",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "83c9e2b5-a802-5f03-cd51-316151535d15",
|
||||||
|
"created_at": "2026-02-25T13:05:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T13:05:00.000Z",
|
||||||
|
"name": "Senior Research Fellow",
|
||||||
|
"description": "Leads academic research projects, secures funding, and mentors doctoral students within the University research ecosystem.",
|
||||||
|
"organization": 1,
|
||||||
|
"members": [2, 7]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.role",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "94d0f3c6-a913-6f14-de62-427262646e26",
|
||||||
|
"created_at": "2026-02-25T13:10:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T13:10:00.000Z",
|
||||||
|
"name": "Quantitative Analyst",
|
||||||
|
"description": "Applies mathematical and statistical methods to financial and risk management problems within the stock simulation model.",
|
||||||
|
"organization": 2,
|
||||||
|
"members": [8, 9]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.role",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "a5e1f4d7-a024-4e25-ef73-538373757f37",
|
||||||
|
"created_at": "2026-02-25T13:15:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T13:15:00.000Z",
|
||||||
|
"name": "Systems Administrator",
|
||||||
|
"description": "Responsible for the maintenance, configuration, and reliable operation of the organization's server infrastructure.",
|
||||||
|
"organization": 2,
|
||||||
|
"members": [1, 12]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.role",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "b6f2e5e8-a135-4f36-fa84-649484868f48",
|
||||||
|
"created_at": "2026-02-25T13:20:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T13:20:00.000Z",
|
||||||
|
"name": "Lead Software Architect",
|
||||||
|
"description": "Responsible for high-level design choices and dictating technical standards, including software coding standards.",
|
||||||
|
"organization": 3,
|
||||||
|
"members": [12, 11]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.role",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "c7f3e6f9-a246-4a47-fa95-750595979f59",
|
||||||
|
"created_at": "2026-02-25T13:25:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T13:25:00.000Z",
|
||||||
|
"name": "FinTech Researcher",
|
||||||
|
"description": "Investigates new technologies in the financial sector, focusing on algorithmic trading and user behavior.",
|
||||||
|
"organization": 3,
|
||||||
|
"members": [9, 10, 3]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "accounts.role",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "d8e4f7f0-a357-4e58-ea06-861606080e60",
|
||||||
|
"created_at": "2026-02-25T13:30:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T13:30:00.000Z",
|
||||||
|
"name": "Compliance Officer",
|
||||||
|
"description": "Ensures that the organization is complying with relevant financial regulations and internal policies.",
|
||||||
|
"organization": 2,
|
||||||
|
"members": [6, 8]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
137
data/4_agentconfigs.json
Normal file
137
data/4_agentconfigs.json
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "onboarding.agentconfig",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "b8ecc397-ac65-4395-9502-b3232ee640e2",
|
||||||
|
"created_at": "2026-02-25T12:51:23.874Z",
|
||||||
|
"updated_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"organization": 2,
|
||||||
|
"name": "UX Developer Curriculum Agent",
|
||||||
|
"agent_type": "curriculum",
|
||||||
|
"llm_config": {"model_id": "meta-llama-3.1-8b-instruct"},
|
||||||
|
"system_prompt": "You are a Senior UX Engineering Mentor. Design a learning path for a UX Developer that bridges the gap between Figma design systems and React component architecture. Focus on accessibility (WCAG), micro-interactions, and state-driven UI patterns.",
|
||||||
|
"tool_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "onboarding.agentconfig",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "33221dd5-3ef9-49a7-98ff-182f3b76f3e6",
|
||||||
|
"created_at": "2026-02-25T12:51:23.875Z",
|
||||||
|
"updated_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"organization": 2,
|
||||||
|
"name": "UX Developer Knowledge Agent",
|
||||||
|
"agent_type": "knowledge",
|
||||||
|
"llm_config": {"model_id": "meta-llama-3.1-8b-instruct"},
|
||||||
|
"system_prompt": "You are a UX Documentation Assistant. Provide technical answers regarding front-end frameworks, CSS-in-JS libraries, and usability testing methodologies. Contextualize answers within the organization's specific design tokens.",
|
||||||
|
"tool_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "onboarding.agentconfig",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "6d50f77a-27d7-4763-8eb8-97b170dde3da",
|
||||||
|
"created_at": "2026-02-25T12:51:23.875Z",
|
||||||
|
"updated_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"organization": 2,
|
||||||
|
"name": "UX Developer Assessment Agent",
|
||||||
|
"agent_type": "assessment",
|
||||||
|
"llm_config": {"model_id": "meta-llama-3.1-8b-instruct"},
|
||||||
|
"system_prompt": "You are a Technical Interviewer for UX Engineers. Generate coding challenges and design critiques that evaluate a user's ability to implement responsive layouts and perform semantic code reviews.",
|
||||||
|
"tool_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "onboarding.agentconfig",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "7ca8091f-ee44-4f03-8abb-95e3d53d5f82",
|
||||||
|
"created_at": "2026-02-25T12:51:23.875Z",
|
||||||
|
"updated_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"organization": 2,
|
||||||
|
"name": "UX Developer Progress Monitor",
|
||||||
|
"agent_type": "monitor",
|
||||||
|
"llm_config": {"model_id": "meta-llama-3.1-8b-instruct"},
|
||||||
|
"system_prompt": "You are a UX Team Lead. Track the completion of design-to-code modules. Identify areas where the developer struggles with specific UI frameworks and suggest remedial design sprints.",
|
||||||
|
"tool_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "onboarding.agentconfig",
|
||||||
|
"pk": 5,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "f29a1b2c-3d4e-5f6a-7b8c-9d0e1f2a3b4c",
|
||||||
|
"created_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"organization": 1,
|
||||||
|
"name": "fNIRS Specialist Curriculum Agent",
|
||||||
|
"agent_type": "curriculum",
|
||||||
|
"llm_config": {"model_id": "meta-llama-3.1-8b-instruct"},
|
||||||
|
"system_prompt": "You are a Neuroimaging Professor. Design a curriculum covering the physics of near-infrared light, optode placement (10-20 system), and the modified Beer-Lambert law. Focus on artifact rejection and signal processing for cortical activation.",
|
||||||
|
"tool_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "onboarding.agentconfig",
|
||||||
|
"pk": 6,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "e30b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
|
||||||
|
"created_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"organization": 1,
|
||||||
|
"name": "fNIRS Specialist Knowledge Agent",
|
||||||
|
"agent_type": "knowledge",
|
||||||
|
"llm_config": {"model_id": "meta-llama-3.1-8b-instruct"},
|
||||||
|
"system_prompt": "You are an fNIRS Lab Assistant. Answer technical queries regarding Homer3, NIRS-Toolbox, and real-time data streaming. Provide troubleshooting steps for high-impedance channels and motion artifacts.",
|
||||||
|
"tool_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "onboarding.agentconfig",
|
||||||
|
"pk": 7,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "d41c3d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
|
||||||
|
"created_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"organization": 1,
|
||||||
|
"name": "fNIRS Specialist Assessment Agent",
|
||||||
|
"agent_type": "assessment",
|
||||||
|
"llm_config": {"model_id": "meta-llama-3.1-8b-instruct"},
|
||||||
|
"system_prompt": "You are a Research Evaluator. Create examination questions that require the student to interpret fNIRS heatmaps and calculate oxygenated vs deoxygenated hemoglobin concentrations from raw OD data.",
|
||||||
|
"tool_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "onboarding.agentconfig",
|
||||||
|
"pk": 8,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "c52d4e5f-6a7b-8c9d-0e1f-2a3b4c5d6e7f",
|
||||||
|
"created_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"organization": 1,
|
||||||
|
"name": "fNIRS Specialist Progress Monitor",
|
||||||
|
"agent_type": "monitor",
|
||||||
|
"llm_config": {"model_id": "meta-llama-3.1-8b-instruct"},
|
||||||
|
"system_prompt": "You are a Principal Investigator. Monitor the student's mastery of fNIRS data collection protocols. Ensure they pass safety and calibration milestones before proceeding to human participant trials.",
|
||||||
|
"tool_permissions": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "onboarding.agentconfig",
|
||||||
|
"pk": 9,
|
||||||
|
"fields": {
|
||||||
|
"uuid": "b63e5f6a-7b8c-9d0e-1f2a-3b4c5d6e7f8a",
|
||||||
|
"created_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"updated_at": "2026-02-25T20:45:00.000Z",
|
||||||
|
"organization": 2,
|
||||||
|
"name": "Quant Analyst Curriculum Agent",
|
||||||
|
"agent_type": "curriculum",
|
||||||
|
"llm_config": {"model_id": "meta-llama-3.1-8b-instruct"},
|
||||||
|
"system_prompt": "You are a Financial Engineering Lead. Design a training path focused on stochastic calculus, Monte Carlo simulations, and Black-Scholes modeling. Emphasize the implementation of these models in Python using NumPy and Pandas.",
|
||||||
|
"tool_permissions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
232
gpu_server.py
Normal file
232
gpu_server.py
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from fastapi import FastAPI, Request, HTTPException
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from llama_cpp import Llama
|
||||||
|
from sentence_transformers import SentenceTransformer
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("gpu-node")
|
||||||
|
|
||||||
|
EMBED_MODEL_NAME = "nomic-ai/nomic-embed-text-v1.5"
|
||||||
|
LLM_MODEL_PATH = os.getenv("LLM_MODEL_PATH", "/app/models/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf")
|
||||||
|
TARGET_DIMENSIONS = 1536
|
||||||
|
|
||||||
|
state: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
"""Handles GPU model loading and cleanup."""
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
logger.info(f"--- Initializing GPU Node on {device} ---")
|
||||||
|
|
||||||
|
if device == "cpu":
|
||||||
|
logger.warning("CUDA NOT DETECTED. Performance will be severely degraded.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load Embedding Model (Nomic)
|
||||||
|
logger.info(f"Loading Embedding Model: {EMBED_MODEL_NAME}")
|
||||||
|
state["embed_model"] = SentenceTransformer(
|
||||||
|
EMBED_MODEL_NAME,
|
||||||
|
trust_remote_code=True,
|
||||||
|
device=device
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load Llama Model (GGUF)
|
||||||
|
if not os.path.exists(LLM_MODEL_PATH):
|
||||||
|
logger.error(f"LLM File not found at {LLM_MODEL_PATH}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Loading LLM: {LLM_MODEL_PATH}")
|
||||||
|
state["llm"] = Llama(
|
||||||
|
model_path=LLM_MODEL_PATH,
|
||||||
|
n_gpu_layers=-1, # Offload all layers to GPU
|
||||||
|
n_ctx=8192,
|
||||||
|
n_batch=512,
|
||||||
|
verbose=False
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("--- GPU Node Ready ---")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to load models: {e}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
state.clear()
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
|
||||||
|
app = FastAPI(title="Agentic GPU Node", lifespan=lifespan)
|
||||||
|
|
||||||
|
|
||||||
|
def pad_and_normalize(embeddings: torch.Tensor) -> torch.Tensor:
|
||||||
|
"""Standardizes vector dimensions to 1536 for pgvector compatibility."""
|
||||||
|
curr_dim = embeddings.shape[1]
|
||||||
|
if curr_dim < TARGET_DIMENSIONS:
|
||||||
|
embeddings = F.pad(embeddings, (0, TARGET_DIMENSIONS - curr_dim), "constant", 0)
|
||||||
|
elif curr_dim > TARGET_DIMENSIONS:
|
||||||
|
embeddings = embeddings[:, :TARGET_DIMENSIONS]
|
||||||
|
return F.normalize(embeddings, p=2, dim=1)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/v1/embeddings")
|
||||||
|
async def embeddings(request: Request):
|
||||||
|
"""Generates text embeddings compatible with OpenAI API format."""
|
||||||
|
data = await request.json()
|
||||||
|
input_data = data.get("input", "")
|
||||||
|
|
||||||
|
if isinstance(input_data, str):
|
||||||
|
inputs = [input_data]
|
||||||
|
elif isinstance(input_data, list):
|
||||||
|
inputs = [str(item) for item in input_data if str(item).strip()]
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail="'input' must be a string or list of strings")
|
||||||
|
|
||||||
|
if not inputs:
|
||||||
|
return {
|
||||||
|
"object": "list",
|
||||||
|
"data": [],
|
||||||
|
"model": EMBED_MODEL_NAME,
|
||||||
|
"usage": {"prompt_tokens": 0, "total_tokens": 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
model = state.get("embed_model")
|
||||||
|
if model is None:
|
||||||
|
raise HTTPException(status_code=503, detail="Embedding model not initialized")
|
||||||
|
|
||||||
|
prefixed_inputs = [
|
||||||
|
text if text.startswith("search_") else f"search_query: {text}"
|
||||||
|
for text in inputs
|
||||||
|
]
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
vectors = model.encode(prefixed_inputs, convert_to_tensor=True)
|
||||||
|
vectors = pad_and_normalize(vectors)
|
||||||
|
|
||||||
|
vector_list = vectors.cpu().tolist()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"object": "list",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"object": "embedding",
|
||||||
|
"index": idx,
|
||||||
|
"embedding": embedding,
|
||||||
|
}
|
||||||
|
for idx, embedding in enumerate(vector_list)
|
||||||
|
],
|
||||||
|
"model": EMBED_MODEL_NAME,
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": sum(len(text.split()) for text in inputs),
|
||||||
|
"total_tokens": sum(len(text.split()) for text in inputs),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/v1/semantic-chunk")
|
||||||
|
async def semantic_chunk(request: Request):
|
||||||
|
"""Processes raw text into semantically cohesive blocks."""
|
||||||
|
data = await request.json()
|
||||||
|
raw_text = data.get("text", "")
|
||||||
|
threshold_percentile = data.get("threshold", 95)
|
||||||
|
|
||||||
|
if not raw_text:
|
||||||
|
return {"chunks": [], "embeddings": []}
|
||||||
|
|
||||||
|
if len(raw_text) > 50000:
|
||||||
|
raise HTTPException(status_code=413, detail="Text block too large. Please batch on the client.")
|
||||||
|
|
||||||
|
model = state.get("embed_model")
|
||||||
|
if model is None:
|
||||||
|
raise HTTPException(status_code=503, detail="Embedding model not initialized")
|
||||||
|
|
||||||
|
# Split by sentences
|
||||||
|
sentences = [s.strip() for s in raw_text.replace('\n', ' ').split('. ') if s.strip()]
|
||||||
|
if len(sentences) < 2:
|
||||||
|
return {
|
||||||
|
"chunks": [raw_text],
|
||||||
|
"embeddings": model.encode([f"search_document: {raw_text}"]).tolist()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate sentence embeddings to find breakpoints via cosine distance
|
||||||
|
s_embeddings = model.encode(sentences, convert_to_tensor=True)
|
||||||
|
distances = [
|
||||||
|
1 - F.cosine_similarity(s_embeddings[i].unsqueeze(0), s_embeddings[i+1].unsqueeze(0)).item()
|
||||||
|
for i in range(len(s_embeddings) - 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
breakpoint_threshold = np.percentile(distances, threshold_percentile)
|
||||||
|
indices = [i for i, d in enumerate(distances) if d > breakpoint_threshold]
|
||||||
|
|
||||||
|
chunks = []
|
||||||
|
start = 0
|
||||||
|
for idx in indices:
|
||||||
|
chunks.append(". ".join(sentences[start : idx + 1]) + ".")
|
||||||
|
start = idx + 1
|
||||||
|
chunks.append(". ".join(sentences[start:]) + ".")
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
final_embeddings = model.encode(
|
||||||
|
[f"search_document: {c}" for c in chunks],
|
||||||
|
convert_to_tensor=True
|
||||||
|
)
|
||||||
|
final_embeddings = pad_and_normalize(final_embeddings)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chunks": chunks,
|
||||||
|
"embeddings": final_embeddings.cpu().tolist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/v1/chat/completions")
|
||||||
|
async def chat_completions(request: Request):
|
||||||
|
"""Unified LLM completion endpoint compatible with OpenAI-style requests."""
|
||||||
|
data = await request.json()
|
||||||
|
messages = data.get("messages", [])
|
||||||
|
stream = data.get("stream", False)
|
||||||
|
|
||||||
|
# Log incoming request details
|
||||||
|
logger.info(f"Chat completion request: {len(messages)} messages, stream={stream}")
|
||||||
|
|
||||||
|
llm = state.get("llm")
|
||||||
|
if not llm:
|
||||||
|
raise HTTPException(status_code=503, detail="LLM not initialized or model file missing.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = llm.create_chat_completion(
|
||||||
|
messages=messages,
|
||||||
|
stream=stream,
|
||||||
|
temperature=data.get("temperature", 0.7),
|
||||||
|
max_tokens=data.get("max_tokens", 1024),
|
||||||
|
stop=["<|eot_id|>", "<|end_of_text|>"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if stream:
|
||||||
|
return StreamingResponse(
|
||||||
|
llm_streamer(response),
|
||||||
|
media_type="text/event-stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Inference error: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
async def llm_streamer(response_iterator):
|
||||||
|
"""Iterates through llama-cpp generator and yields SSE chunks."""
|
||||||
|
for chunk in response_iterator:
|
||||||
|
yield f"data: {json.dumps(chunk)}\n\n"
|
||||||
|
yield "data: [DONE]\n\n"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("gpu_server:app", host="0.0.0.0", port=8001, reload=True)
|
||||||
22
manage.py
Normal file
22
manage.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
146
notebooks/external-model-testing.ipynb
Normal file
146
notebooks/external-model-testing.ipynb
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "0910db83",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Model Testing with GPT4ALL running locally"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 6,
|
||||||
|
"id": "47cacfc9",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Imports\n",
|
||||||
|
"import json\n",
|
||||||
|
"import requests"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"id": "484cfebc",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# Variables for model response\n",
|
||||||
|
"API_URL = \"http://localhost:4891/v1/chat/completions\"\n",
|
||||||
|
"HEADERS = {\"Content-Type\": \"application/json\"}\n",
|
||||||
|
"MODEL = \"DeepSeek-R1-Distill-Qwen-7B\"\n",
|
||||||
|
"MAX_TOKENS = 2000\n",
|
||||||
|
"TEMPERATURE = 0.28"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 8,
|
||||||
|
"id": "90b9b1f1",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"content = \"Teach me computer vision\"\n",
|
||||||
|
"data = {\"model\": MODEL,\"messages\":[{\"role\":\"user\",\"content\": content}],\"max_tokens\": MAX_TOKENS,\"temperature\": TEMPERATURE}\n",
|
||||||
|
"response = requests.post(API_URL, json = data, headers=HEADERS)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 9,
|
||||||
|
"id": "88a77498",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"'{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"logprobs\":null,\"message\":{\"content\":\"<think>\\\\n\\\\n</think>\\\\n\\\\nComputer vision is a field of artificial intelligence that focuses on enabling computers to interpret and understand visual information from the world. It involves training algorithms, typically using deep learning techniques, to perform tasks such as object recognition, image segmentation, feature extraction, and more.\\\\n\\\\nHere’s an introduction to get you started:\\\\n\\\\n---\\\\n\\\\n### **1. What is Computer Vision?**\\\\nComputer vision mimics human visual perception by analyzing images or video data to extract meaningful information. It relies heavily on machine learning and deep learning techniques like convolutional neural networks (CNNs) to perform tasks such as:\\\\n- Object detection\\\\n- Image classification\\\\n- Face recognition\\\\n- Medical image analysis\\\\n- Autonomous vehicle navigation\\\\n\\\\n---\\\\n\\\\n### **2. Key Concepts in Computer Vision**\\\\n#### **Image Representation**\\\\n- **Pixels**: The basic unit of an image, represented by numerical values indicating color and brightness.\\\\n- **Channels**: Color images have multiple channels (e.g., RGB has red, green, blue channels).\\\\n\\\\n#### **Common Tasks**\\\\n1. **Object Detection**:\\\\n - Identify the presence and location of objects in an image.\\\\n - Example: Bounding box regression.\\\\n\\\\n2. **Classification**:\\\\n - Categorize images into predefined classes (e.g., cat vs. dog).\\\\n\\\\n3. **Segmentation**:\\\\n - Partition an image into segments, each representing a different object or region.\\\\n\\\\n4. **Feature Extraction**:\\\\n - Identify and extract relevant patterns from images for further analysis.\\\\n\\\\n---\\\\n\\\\n### **3. Tools and Libraries**\\\\nTo get started with computer vision, you’ll need tools like OpenCV (Open Source Computer Vision) or TensorFlow/Keras for building models.\\\\n\\\\n#### **OpenCV**\\\\n- A popular open-source library for image processing.\\\\n- Features:\\\\n - Image filtering\\\\n - Edge detection\\\\n - Object tracking\\\\n - Face recognition\\\\n\\\\n#### **TensorFlow/Keras**\\\\n- Frameworks built on top of TensorFlow, ideal for deep learning tasks.\\\\n- Easy to use and widely adopted.\\\\n\\\\n---\\\\n\\\\n### **4. Getting Started with Computer Vision**\\\\n\\\\n#### **Step 1: Learn the Basics**\\\\nStart by understanding fundamental concepts like pixels, image processing techniques, and basic computer vision algorithms (e.g., SIFT, HOG).\\\\n\\\\n#### **Step 2: Explore Datasets**\\\\nWork with common datasets:\\\\n- CIFAR-10/100\\\\n- MNIST (handwritten digits)\\\\n- COCO (common objects in context)\\\\n\\\\n#### **Step 3: Build Simple Models**\\\\nUse pre-trained models like ResNet or VGG to classify images. For example, you can train a model to recognize cats vs. dogs.\\\\n\\\\n#### **Step 4: Experiment with Deep Learning**\\\\nTune hyperparameters (learning rate, batch size) and explore techniques like data augmentation to improve model performance.\\\\n\\\\n---\\\\n\\\\n### **5. Resources for Learning**\\\\n- **Books**:\\\\n - *Deep Learning for Computer Vision* by Adrian Rosebrock\\\\n - *Computer Vision: Algorithms and Applications* by Richard Szeliski\\\\n\\\\n- **Tutorials/Documentation**:\\\\n - OpenCV官网文档 [https://docs.opencv.org](https://docs.opencv.org)\\\\n - TensorFlow/Keras官网文档 [https://www.tensorflow.org](https://www.tensorflow.org)\\\\n\\\\n- **Online Courses**:\\\\n - Coursera: \\\\\"Introduction to Computer Vision\\\\\" by Georgia Tech\\\\n - Udacity: \\\\\"Deep Learning for Computer Vision\\\\\"\\\\n - Fast.ai: Free, practical courses on computer vision.\\\\n\\\\n---\\\\n\\\\n### **6. Practice Projects**\\\\n1. **Object Detection**: Use YOLO or Mask R-CNN to detect objects in images.\\\\n2. **Image Classification**: Build a model that classifies images into predefined categories (e.g., flowers vs. vegetables).\\\\n3. **Face Recognition**: Implement face recognition using deep learning frameworks.\\\\n\\\\n---\\\\n\\\\n### **7. Keep Learning**\\\\n- Follow research papers on arXiv ([https://arxiv.org](https://arxiv.org)).\\\\n- Join communities like Reddit’s r/computervision or Stack Overflow.\\\\n- Experiment with cutting-edge models and techniques in computer vision.\\\\n\\\\n---\\\\n\\\\nWith practice and persistence, you’ll become proficient in computer vision. Start small, experiment, and most importantly, have fun!\",\"role\":\"assistant\"},\"references\":null}],\"created\":1768678056,\"id\":\"placeholder\",\"model\":\"DeepSeek-R1-Distill-Qwen-7B\",\"object\":\"chat.completion\",\"usage\":{\"completion_tokens\":861,\"prompt_tokens\":8,\"total_tokens\":869}}'"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 9,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"response.text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 10,
|
||||||
|
"id": "c416905c",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"{'choices': [{'finish_reason': 'stop',\n",
|
||||||
|
" 'index': 0,\n",
|
||||||
|
" 'logprobs': None,\n",
|
||||||
|
" 'message': {'content': '<think>\\n\\n</think>\\n\\nComputer vision is a field of artificial intelligence that focuses on enabling computers to interpret and understand visual information from the world. It involves training algorithms, typically using deep learning techniques, to perform tasks such as object recognition, image segmentation, feature extraction, and more.\\n\\nHere’s an introduction to get you started:\\n\\n---\\n\\n### **1. What is Computer Vision?**\\nComputer vision mimics human visual perception by analyzing images or video data to extract meaningful information. It relies heavily on machine learning and deep learning techniques like convolutional neural networks (CNNs) to perform tasks such as:\\n- Object detection\\n- Image classification\\n- Face recognition\\n- Medical image analysis\\n- Autonomous vehicle navigation\\n\\n---\\n\\n### **2. Key Concepts in Computer Vision**\\n#### **Image Representation**\\n- **Pixels**: The basic unit of an image, represented by numerical values indicating color and brightness.\\n- **Channels**: Color images have multiple channels (e.g., RGB has red, green, blue channels).\\n\\n#### **Common Tasks**\\n1. **Object Detection**:\\n - Identify the presence and location of objects in an image.\\n - Example: Bounding box regression.\\n\\n2. **Classification**:\\n - Categorize images into predefined classes (e.g., cat vs. dog).\\n\\n3. **Segmentation**:\\n - Partition an image into segments, each representing a different object or region.\\n\\n4. **Feature Extraction**:\\n - Identify and extract relevant patterns from images for further analysis.\\n\\n---\\n\\n### **3. Tools and Libraries**\\nTo get started with computer vision, you’ll need tools like OpenCV (Open Source Computer Vision) or TensorFlow/Keras for building models.\\n\\n#### **OpenCV**\\n- A popular open-source library for image processing.\\n- Features:\\n - Image filtering\\n - Edge detection\\n - Object tracking\\n - Face recognition\\n\\n#### **TensorFlow/Keras**\\n- Frameworks built on top of TensorFlow, ideal for deep learning tasks.\\n- Easy to use and widely adopted.\\n\\n---\\n\\n### **4. Getting Started with Computer Vision**\\n\\n#### **Step 1: Learn the Basics**\\nStart by understanding fundamental concepts like pixels, image processing techniques, and basic computer vision algorithms (e.g., SIFT, HOG).\\n\\n#### **Step 2: Explore Datasets**\\nWork with common datasets:\\n- CIFAR-10/100\\n- MNIST (handwritten digits)\\n- COCO (common objects in context)\\n\\n#### **Step 3: Build Simple Models**\\nUse pre-trained models like ResNet or VGG to classify images. For example, you can train a model to recognize cats vs. dogs.\\n\\n#### **Step 4: Experiment with Deep Learning**\\nTune hyperparameters (learning rate, batch size) and explore techniques like data augmentation to improve model performance.\\n\\n---\\n\\n### **5. Resources for Learning**\\n- **Books**:\\n - *Deep Learning for Computer Vision* by Adrian Rosebrock\\n - *Computer Vision: Algorithms and Applications* by Richard Szeliski\\n\\n- **Tutorials/Documentation**:\\n - OpenCV官网文档 [https://docs.opencv.org](https://docs.opencv.org)\\n - TensorFlow/Keras官网文档 [https://www.tensorflow.org](https://www.tensorflow.org)\\n\\n- **Online Courses**:\\n - Coursera: \"Introduction to Computer Vision\" by Georgia Tech\\n - Udacity: \"Deep Learning for Computer Vision\"\\n - Fast.ai: Free, practical courses on computer vision.\\n\\n---\\n\\n### **6. Practice Projects**\\n1. **Object Detection**: Use YOLO or Mask R-CNN to detect objects in images.\\n2. **Image Classification**: Build a model that classifies images into predefined categories (e.g., flowers vs. vegetables).\\n3. **Face Recognition**: Implement face recognition using deep learning frameworks.\\n\\n---\\n\\n### **7. Keep Learning**\\n- Follow research papers on arXiv ([https://arxiv.org](https://arxiv.org)).\\n- Join communities like Reddit’s r/computervision or Stack Overflow.\\n- Experiment with cutting-edge models and techniques in computer vision.\\n\\n---\\n\\nWith practice and persistence, you’ll become proficient in computer vision. Start small, experiment, and most importantly, have fun!',\n",
|
||||||
|
" 'role': 'assistant'},\n",
|
||||||
|
" 'references': None}],\n",
|
||||||
|
" 'created': 1768678056,\n",
|
||||||
|
" 'id': 'placeholder',\n",
|
||||||
|
" 'model': 'DeepSeek-R1-Distill-Qwen-7B',\n",
|
||||||
|
" 'object': 'chat.completion',\n",
|
||||||
|
" 'usage': {'completion_tokens': 861, 'prompt_tokens': 8, 'total_tokens': 869}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 10,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"response_data = json.loads(response.text)\n",
|
||||||
|
"response_data"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 11,
|
||||||
|
"id": "2553d924",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"'<think>\\n\\n</think>\\n\\nComputer vision is a field of artificial intelligence that focuses on enabling computers to interpret and understand visual information from the world. It involves training algorithms, typically using deep learning techniques, to perform tasks such as object recognition, image segmentation, feature extraction, and more.\\n\\nHere’s an introduction to get you started:\\n\\n---\\n\\n### **1. What is Computer Vision?**\\nComputer vision mimics human visual perception by analyzing images or video data to extract meaningful information. It relies heavily on machine learning and deep learning techniques like convolutional neural networks (CNNs) to perform tasks such as:\\n- Object detection\\n- Image classification\\n- Face recognition\\n- Medical image analysis\\n- Autonomous vehicle navigation\\n\\n---\\n\\n### **2. Key Concepts in Computer Vision**\\n#### **Image Representation**\\n- **Pixels**: The basic unit of an image, represented by numerical values indicating color and brightness.\\n- **Channels**: Color images have multiple channels (e.g., RGB has red, green, blue channels).\\n\\n#### **Common Tasks**\\n1. **Object Detection**:\\n - Identify the presence and location of objects in an image.\\n - Example: Bounding box regression.\\n\\n2. **Classification**:\\n - Categorize images into predefined classes (e.g., cat vs. dog).\\n\\n3. **Segmentation**:\\n - Partition an image into segments, each representing a different object or region.\\n\\n4. **Feature Extraction**:\\n - Identify and extract relevant patterns from images for further analysis.\\n\\n---\\n\\n### **3. Tools and Libraries**\\nTo get started with computer vision, you’ll need tools like OpenCV (Open Source Computer Vision) or TensorFlow/Keras for building models.\\n\\n#### **OpenCV**\\n- A popular open-source library for image processing.\\n- Features:\\n - Image filtering\\n - Edge detection\\n - Object tracking\\n - Face recognition\\n\\n#### **TensorFlow/Keras**\\n- Frameworks built on top of TensorFlow, ideal for deep learning tasks.\\n- Easy to use and widely adopted.\\n\\n---\\n\\n### **4. Getting Started with Computer Vision**\\n\\n#### **Step 1: Learn the Basics**\\nStart by understanding fundamental concepts like pixels, image processing techniques, and basic computer vision algorithms (e.g., SIFT, HOG).\\n\\n#### **Step 2: Explore Datasets**\\nWork with common datasets:\\n- CIFAR-10/100\\n- MNIST (handwritten digits)\\n- COCO (common objects in context)\\n\\n#### **Step 3: Build Simple Models**\\nUse pre-trained models like ResNet or VGG to classify images. For example, you can train a model to recognize cats vs. dogs.\\n\\n#### **Step 4: Experiment with Deep Learning**\\nTune hyperparameters (learning rate, batch size) and explore techniques like data augmentation to improve model performance.\\n\\n---\\n\\n### **5. Resources for Learning**\\n- **Books**:\\n - *Deep Learning for Computer Vision* by Adrian Rosebrock\\n - *Computer Vision: Algorithms and Applications* by Richard Szeliski\\n\\n- **Tutorials/Documentation**:\\n - OpenCV官网文档 [https://docs.opencv.org](https://docs.opencv.org)\\n - TensorFlow/Keras官网文档 [https://www.tensorflow.org](https://www.tensorflow.org)\\n\\n- **Online Courses**:\\n - Coursera: \"Introduction to Computer Vision\" by Georgia Tech\\n - Udacity: \"Deep Learning for Computer Vision\"\\n - Fast.ai: Free, practical courses on computer vision.\\n\\n---\\n\\n### **6. Practice Projects**\\n1. **Object Detection**: Use YOLO or Mask R-CNN to detect objects in images.\\n2. **Image Classification**: Build a model that classifies images into predefined categories (e.g., flowers vs. vegetables).\\n3. **Face Recognition**: Implement face recognition using deep learning frameworks.\\n\\n---\\n\\n### **7. Keep Learning**\\n- Follow research papers on arXiv ([https://arxiv.org](https://arxiv.org)).\\n- Join communities like Reddit’s r/computervision or Stack Overflow.\\n- Experiment with cutting-edge models and techniques in computer vision.\\n\\n---\\n\\nWith practice and persistence, you’ll become proficient in computer vision. Start small, experiment, and most importantly, have fun!'"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 11,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"response_data['choices'][0]['message']['content']"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": ".venv",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
1210
notebooks/fine-tune-local-model.ipynb
Normal file
1210
notebooks/fine-tune-local-model.ipynb
Normal file
File diff suppressed because it is too large
Load diff
353
notebooks/local-model-rag-implementation.ipynb
Normal file
353
notebooks/local-model-rag-implementation.ipynb
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "45d62106",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Basic RAG Implementation with a local LLM"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"id": "4c312410",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from gpt4all import GPT4All\n",
|
||||||
|
"from sentence_transformers import SentenceTransformer\n",
|
||||||
|
"from chromadb import PersistentClient\n",
|
||||||
|
"from docx import Document\n",
|
||||||
|
"\n",
|
||||||
|
"MODEL = \"Meta-Llama-3-8B-Instruct.Q4_0.gguf\"\n",
|
||||||
|
"CONTEXT_SIZE = 8192\n",
|
||||||
|
"EMBEDDER = \"all-MiniLM-L6-v2\"\n",
|
||||||
|
"RAG_PATH = \"./build/rag_db\"\n",
|
||||||
|
"DOCS_PATH = \"./build/documents/fNIRS_Glossary_Hardware.docx\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"id": "90bae527",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "104f2001edc34aa5aff82734b3388041",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"modules.json: 0%| | 0.00/349 [00:00<?, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stderr",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"c:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\huggingface_hub\\file_download.py:143: UserWarning: `huggingface_hub` cache-system uses symlinks by default to efficiently store duplicated files but your machine does not support them in C:\\Users\\nalab\\.cache\\huggingface\\hub\\models--sentence-transformers--all-MiniLM-L6-v2. Caching files will still work but in a degraded version that might require more space on your disk. This warning can be disabled by setting the `HF_HUB_DISABLE_SYMLINKS_WARNING` environment variable. For more details, see https://huggingface.co/docs/huggingface_hub/how-to-cache#limitations.\n",
|
||||||
|
"To support symlinks on Windows, you either need to activate Developer Mode or to run Python as an administrator. In order to activate developer mode, see this article: https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development\n",
|
||||||
|
" warnings.warn(message)\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "7bf16ea40d964be19217eadc81f5674e",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"config_sentence_transformers.json: 0%| | 0.00/116 [00:00<?, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "32962e77048440908808689c5dc386e0",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"README.md: 0.00B [00:00, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "bf08ffecdfa94eaca2841e2b6b88eea5",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"sentence_bert_config.json: 0%| | 0.00/53.0 [00:00<?, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "6079ecdd0e464623a1d7e20999213213",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"config.json: 0%| | 0.00/612 [00:00<?, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "60b2de9bec5c4237827d910660389db1",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"model.safetensors: 0%| | 0.00/90.9M [00:00<?, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "05f352a112fb4ccd8968a7ffe335c80f",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"tokenizer_config.json: 0%| | 0.00/350 [00:00<?, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "b5f7aa6547c0455eb55863ad8ec6c84f",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"vocab.txt: 0.00B [00:00, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "43605d598a604c10a85effee5869939e",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"tokenizer.json: 0.00B [00:00, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "bd1a21fcccee4a92a50dcca08c858565",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"special_tokens_map.json: 0%| | 0.00/112 [00:00<?, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"application/vnd.jupyter.widget-view+json": {
|
||||||
|
"model_id": "6d409c5032674774bfe157e1ec21eb3a",
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
},
|
||||||
|
"text/plain": [
|
||||||
|
"config.json: 0%| | 0.00/190 [00:00<?, ?B/s]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "display_data"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"model = GPT4All(model_name = MODEL, n_ctx = CONTEXT_SIZE, allow_download = True, device = \"cuda\")\n",
|
||||||
|
"embedder = SentenceTransformer(EMBEDDER)\n",
|
||||||
|
"client = PersistentClient(path = RAG_PATH)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"class EmbeddingFunctionWrapper:\n",
|
||||||
|
" def __init__(self, model):\n",
|
||||||
|
" self.model = model\n",
|
||||||
|
"\n",
|
||||||
|
" def name(self):\n",
|
||||||
|
" return \"sentence-transformers\"\n",
|
||||||
|
"\n",
|
||||||
|
" def __call__(self, input):\n",
|
||||||
|
" if isinstance(input, str):\n",
|
||||||
|
" texts = [input]\n",
|
||||||
|
" embs = self.model.encode(texts).tolist()\n",
|
||||||
|
" return embs[0]\n",
|
||||||
|
" else:\n",
|
||||||
|
" texts = list(input)\n",
|
||||||
|
" return self.model.encode(texts).tolist()\n",
|
||||||
|
"\n",
|
||||||
|
"embedding_fn = EmbeddingFunctionWrapper(embedder)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"id": "34efbc7c",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"doc = Document(DOCS_PATH)\n",
|
||||||
|
"docx_content = \"\\n\".join([paragraph.text for paragraph in doc.paragraphs if paragraph.text.strip()])\n",
|
||||||
|
"chunk_size = 1000\n",
|
||||||
|
"documents = [docx_content[i:i+chunk_size] for i in range(0, len(docx_content), chunk_size) if docx_content[i:i+chunk_size].strip()]\n",
|
||||||
|
"embeddings = embedder.encode(documents).tolist()\n",
|
||||||
|
"collection = client.get_or_create_collection(\n",
|
||||||
|
" name = \"knowledge_base\",\n",
|
||||||
|
" embedding_function = embedding_fn,\n",
|
||||||
|
")\n",
|
||||||
|
"collection.add(\n",
|
||||||
|
" documents=documents,\n",
|
||||||
|
" embeddings=embeddings,\n",
|
||||||
|
" ids=[f\"doc{i}\" for i in range(len(documents))]\n",
|
||||||
|
")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 4,
|
||||||
|
"id": "ed2cc1ff",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def retrieve(query, top_k = 1):\n",
|
||||||
|
" query_embedding = embedder.encode([query]).tolist()[0]\n",
|
||||||
|
" try:\n",
|
||||||
|
" results = collection.query(query_texts=[query], n_results=top_k)\n",
|
||||||
|
" return results[\"documents\"][0]\n",
|
||||||
|
" except Exception:\n",
|
||||||
|
" results = collection.query(query_embeddings=[query_embedding], n_results=top_k)\n",
|
||||||
|
" return results[\"documents\"][0]\n",
|
||||||
|
"\n",
|
||||||
|
"def rag_answer(query):\n",
|
||||||
|
" retrieved_docs = retrieve(query)\n",
|
||||||
|
" context = \"\\n\\n\".join(retrieved_docs)\n",
|
||||||
|
" max_context_length = 500\n",
|
||||||
|
" if len(context) > max_context_length:\n",
|
||||||
|
" context = context[:max_context_length] + \"...\"\n",
|
||||||
|
"\n",
|
||||||
|
" prompt = f\"\"\"\n",
|
||||||
|
"Use the context to answer the question.\n",
|
||||||
|
"Context:\n",
|
||||||
|
"{context}\n",
|
||||||
|
"Question:\n",
|
||||||
|
"{query}\n",
|
||||||
|
"Answer:\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
" print(f\"Prompt length: {len(prompt)}\")\n",
|
||||||
|
" return model.generate(prompt, max_tokens=200)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"id": "6fa9fd10",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Number of documents: 68\n",
|
||||||
|
"Document lengths: [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 63]\n",
|
||||||
|
"Retrieved docs length: 1\n",
|
||||||
|
"Prompt length: 627\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"query = \"What can Frequency domain multidistance NIRS estimate?\"\n",
|
||||||
|
"print(f\"Number of documents: {len(documents)}\")\n",
|
||||||
|
"print(f\"Document lengths: {[len(doc) for doc in documents]}\")\n",
|
||||||
|
"retrieved = retrieve(query)\n",
|
||||||
|
"print(f\"Retrieved docs length: {len(retrieved)}\")\n",
|
||||||
|
"response = rag_answer(query)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 6,
|
||||||
|
"id": "5a82353e",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"'Frequency-domain (FD) multidistance NIRS technique can estimate absolute values of absorption and scattering of the medium, and subsequently chromophore concentrations.'"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 6,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"response"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": ".venv",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
583
notebooks/prepare-training-file.ipynb
Normal file
583
notebooks/prepare-training-file.ipynb
Normal file
|
|
@ -0,0 +1,583 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "c9cd197e",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Prepare Training File: Load Model & Generate Training Pairs\n",
|
||||||
|
"\n",
|
||||||
|
"This notebook loads a language model and uses it to generate structured instruction/response training pairs from any input file. The generated pairs can be used directly for fine-tuning."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "556d3fe5",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Setup: Environment Variables\n",
|
||||||
|
"\n",
|
||||||
|
"Configure CUDA and PyTorch environment variables to disable BF16 and FP16 precision reductions for stable training."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "a25b6a3b",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import os\n",
|
||||||
|
"os.environ[\"CUDA_DISABLE_BF16\"] = \"1\"\n",
|
||||||
|
"os.environ[\"TORCH_CUDA_ALLOW_BF16_REDUCED_PRECISION_REDUCTION\"] = \"0\"\n",
|
||||||
|
"os.environ[\"ACCELERATE_DISABLE_FP16\"] = \"1\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "97b9e212",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Setup: Import Required Libraries\n",
|
||||||
|
"\n",
|
||||||
|
"Import necessary libraries including transformers, torch, datasets, python-docx, json, os, and other utilities for document processing and model loading."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "0d63d552",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import json\n",
|
||||||
|
"import logging\n",
|
||||||
|
"import os\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"\n",
|
||||||
|
"from docx import Document\n",
|
||||||
|
"from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig\n",
|
||||||
|
"import torch\n",
|
||||||
|
"\n",
|
||||||
|
"logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n",
|
||||||
|
"logger = logging.getLogger(__name__)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "84e04da2",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Setup: Configure Directory Structure\n",
|
||||||
|
"\n",
|
||||||
|
"Create and organize directory paths for storing training data, models, and intermediate outputs."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "993ed003",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"OUTPUT_DIR = Path(\"./build/training_prep\")\n",
|
||||||
|
"OUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n",
|
||||||
|
"DATA_DIR = OUTPUT_DIR / \"data\"\n",
|
||||||
|
"DATA_DIR.mkdir(exist_ok=True)\n",
|
||||||
|
"MODELS_DIR = OUTPUT_DIR / \"models\"\n",
|
||||||
|
"MODELS_DIR.mkdir(exist_ok=True)\n",
|
||||||
|
"\n",
|
||||||
|
"MODEL_CACHE_DIR = Path(\"./model/base-model\")\n",
|
||||||
|
"MODEL_CACHE_DIR.mkdir(parents=True, exist_ok=True)\n",
|
||||||
|
"os.environ[\"HF_HOME\"] = str(MODEL_CACHE_DIR)\n",
|
||||||
|
"\n",
|
||||||
|
"logger.info(f\"Output directory: {OUTPUT_DIR}\")\n",
|
||||||
|
"logger.info(f\"Model cache directory: {MODEL_CACHE_DIR}\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "0439c534",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Setup: Helper Functions\n",
|
||||||
|
"\n",
|
||||||
|
"Define utility functions for loading various file formats (DOCX, JSON, JSONL)."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "e34ff2b7",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def load_docx_file(file_path: str) -> list:\n",
|
||||||
|
" \"\"\"Load and parse a DOCX file into paragraphs.\"\"\"\n",
|
||||||
|
" logger.info(f\"Loading DOCX file: {file_path}\")\n",
|
||||||
|
" doc = Document(file_path)\n",
|
||||||
|
" paragraphs = [p.text.strip() for p in doc.paragraphs if p.text.strip()]\n",
|
||||||
|
" logger.info(f\"Extracted {len(paragraphs)} paragraphs from {file_path}\")\n",
|
||||||
|
" return paragraphs\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def load_json_file(file_path: str) -> list:\n",
|
||||||
|
" \"\"\"Load a JSON file (array or object).\"\"\"\n",
|
||||||
|
" logger.info(f\"Loading JSON file: {file_path}\")\n",
|
||||||
|
" with open(file_path, 'r', encoding='utf-8') as f:\n",
|
||||||
|
" data = json.load(f)\n",
|
||||||
|
" if isinstance(data, list):\n",
|
||||||
|
" logger.info(f\"Loaded {len(data)} items from JSON file\")\n",
|
||||||
|
" return data\n",
|
||||||
|
" elif isinstance(data, dict):\n",
|
||||||
|
" logger.info(f\"JSON file is dict, converting to list\")\n",
|
||||||
|
" return [data]\n",
|
||||||
|
" return []\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def load_jsonl_file(file_path: str) -> list:\n",
|
||||||
|
" \"\"\"Load a JSONL file (one JSON object per line).\"\"\"\n",
|
||||||
|
" logger.info(f\"Loading JSONL file: {file_path}\")\n",
|
||||||
|
" items = []\n",
|
||||||
|
" with open(file_path, 'r', encoding='utf-8') as f:\n",
|
||||||
|
" for line in f:\n",
|
||||||
|
" if line.strip():\n",
|
||||||
|
" items.append(json.loads(line))\n",
|
||||||
|
" logger.info(f\"Loaded {len(items)} items from JSONL file\")\n",
|
||||||
|
" return items\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def load_training_file(file_path: str) -> list:\n",
|
||||||
|
" \"\"\"Load training file based on extension.\"\"\"\n",
|
||||||
|
" ext = Path(file_path).suffix.lower()\n",
|
||||||
|
" if ext == '.docx':\n",
|
||||||
|
" return load_docx_file(file_path)\n",
|
||||||
|
" elif ext == '.json':\n",
|
||||||
|
" return load_json_file(file_path)\n",
|
||||||
|
" elif ext == '.jsonl':\n",
|
||||||
|
" return load_jsonl_file(file_path)\n",
|
||||||
|
" else:\n",
|
||||||
|
" raise ValueError(f\"Unsupported file format: {ext}\")\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"logger.info(\"Helper functions defined\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "3bea7ee7",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Step 1: Load and Configure the Base Model\n",
|
||||||
|
"\n",
|
||||||
|
"Load Meta-Llama-3-8B-Instruct with 4-bit quantization for efficient pair generation. The model will read your input file and generate formatted instruction/response pairs."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "0348d7d6",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"if not torch.cuda.is_available():\n",
|
||||||
|
" raise RuntimeError(\"CUDA not available. Please run in a GPU environment.\")\n",
|
||||||
|
"\n",
|
||||||
|
"logger.info(f\"Using GPU: {torch.cuda.get_device_name(0)}\")\n",
|
||||||
|
"\n",
|
||||||
|
"BASE_MODEL = \"meta-llama/Meta-Llama-3-8B-Instruct\"\n",
|
||||||
|
"\n",
|
||||||
|
"logger.info(f\"Loading base model: {BASE_MODEL}\")\n",
|
||||||
|
"tokenizer = AutoTokenizer.from_pretrained(\n",
|
||||||
|
" BASE_MODEL,\n",
|
||||||
|
" cache_dir=str(MODEL_CACHE_DIR),\n",
|
||||||
|
" local_files_only=False,\n",
|
||||||
|
")\n",
|
||||||
|
"if tokenizer.pad_token is None:\n",
|
||||||
|
" tokenizer.pad_token = tokenizer.eos_token\n",
|
||||||
|
"\n",
|
||||||
|
"model = AutoModelForCausalLM.from_pretrained(\n",
|
||||||
|
" BASE_MODEL,\n",
|
||||||
|
" cache_dir=str(MODEL_CACHE_DIR),\n",
|
||||||
|
" quantization_config=BitsAndBytesConfig(\n",
|
||||||
|
" load_in_4bit=True,\n",
|
||||||
|
" bnb_4bit_compute_dtype=torch.float16\n",
|
||||||
|
" ),\n",
|
||||||
|
" device_map=\"auto\",\n",
|
||||||
|
" dtype=torch.float16,\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"logger.info(\"Model loaded successfully\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "bbb7155b",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Step 2: Load Your Training File\n",
|
||||||
|
"\n",
|
||||||
|
"Specify the path to your training file (DOCX, JSON, or JSONL). The notebook will parse it and prepare it for pair generation."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "fe29c8b2",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"TRAINING_FILE = \"./model/data/data.docx\"\n",
|
||||||
|
"training_data = load_training_file(TRAINING_FILE)\n",
|
||||||
|
"logger.info(f\"Loaded {len(training_data)} items from training file\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "70aa4949",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"print(f\"Loaded {len(training_data)} items\")\n",
|
||||||
|
"print(f\"First item type: {type(training_data[0])}\")\n",
|
||||||
|
"print(f\"First item (first 200 chars): {str(training_data[0])[:200]}\")\n",
|
||||||
|
"if isinstance(training_data[0], dict):\n",
|
||||||
|
" print(f\"First item keys: {list(training_data[0].keys())}\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "cdfdaa4d",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Step 3: Generate Training Pairs Using the Model\n",
|
||||||
|
"\n",
|
||||||
|
"The model will read your data and generate structured instruction/response pairs using a prompt-based approach. This ensures consistent formatting for fine-tuning."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "f36ab365",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def format_training_sample(sample) -> str:\n",
|
||||||
|
" \"\"\"Convert a training item into a concise text description.\"\"\"\n",
|
||||||
|
" try:\n",
|
||||||
|
" if isinstance(sample, dict):\n",
|
||||||
|
" parts = []\n",
|
||||||
|
" for k, v in sample.items():\n",
|
||||||
|
" if isinstance(v, str) and v.strip():\n",
|
||||||
|
" parts.append(f\"{k}: {v.strip()}\")\n",
|
||||||
|
" return \" | \".join(parts) if parts else json.dumps(sample)\n",
|
||||||
|
" if isinstance(sample, str):\n",
|
||||||
|
" return sample.strip()\n",
|
||||||
|
" return str(sample)\n",
|
||||||
|
" except Exception:\n",
|
||||||
|
" return str(sample)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def get_optimal_batch_size() -> int:\n",
|
||||||
|
" \"\"\"Calculate optimal batch size based on available GPU memory.\"\"\"\n",
|
||||||
|
" if not torch.cuda.is_available():\n",
|
||||||
|
" return 5\n",
|
||||||
|
"\n",
|
||||||
|
" try:\n",
|
||||||
|
" gpu_mem = torch.cuda.get_device_properties(0).total_memory / (1024**3)\n",
|
||||||
|
"\n",
|
||||||
|
" logger.info(f\"GPU total memory: {gpu_mem:.2f} GB\")\n",
|
||||||
|
"\n",
|
||||||
|
" if gpu_mem >= 24:\n",
|
||||||
|
" return 20\n",
|
||||||
|
" elif gpu_mem >= 16:\n",
|
||||||
|
" return 15\n",
|
||||||
|
" elif gpu_mem >= 12:\n",
|
||||||
|
" return 12\n",
|
||||||
|
" elif gpu_mem >= 8:\n",
|
||||||
|
" return 8\n",
|
||||||
|
" else:\n",
|
||||||
|
" return 5\n",
|
||||||
|
" except Exception as e:\n",
|
||||||
|
" logger.warning(f\"Could not determine GPU memory: {e}. Using conservative batch size.\")\n",
|
||||||
|
" return 5\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"def generate_pairs_with_model(training_data: list, batch_size: int = None, max_tokens: int = 2048) -> list:\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" Use the model to generate instruction/response pairs from training data.\n",
|
||||||
|
" Processes data in batches to fit within GPU memory constraints.\n",
|
||||||
|
"\n",
|
||||||
|
" Args:\n",
|
||||||
|
" training_data: List of training items to process\n",
|
||||||
|
" batch_size: Number of items per batch (None = auto-detect based on GPU memory)\n",
|
||||||
|
" max_tokens: Maximum tokens to generate per batch (default: 2048)\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" if batch_size is None:\n",
|
||||||
|
" batch_size = get_optimal_batch_size()\n",
|
||||||
|
"\n",
|
||||||
|
" logger.info(f\"Generating training pairs from {len(training_data)} items\")\n",
|
||||||
|
" logger.info(f\"Batch size: {batch_size}, Max tokens per batch: {max_tokens}\")\n",
|
||||||
|
"\n",
|
||||||
|
" all_pairs = []\n",
|
||||||
|
"\n",
|
||||||
|
" DEBUG_OUTPUT = False\n",
|
||||||
|
"\n",
|
||||||
|
" for i in range(0, len(training_data), batch_size):\n",
|
||||||
|
" batch = training_data[i:i+batch_size]\n",
|
||||||
|
" batch_num = i//batch_size + 1\n",
|
||||||
|
" total_batches = (len(training_data) + batch_size - 1)//batch_size\n",
|
||||||
|
"\n",
|
||||||
|
" logger.info(f\"Processing batch {batch_num}/{total_batches} ({len(batch)} items)\")\n",
|
||||||
|
"\n",
|
||||||
|
" formatted = [f\"{j+1}. {format_training_sample(item)}\" for j, item in enumerate(batch)]\n",
|
||||||
|
" data_block = \"\\n\".join(formatted)\n",
|
||||||
|
"\n",
|
||||||
|
" system_prompt = (\n",
|
||||||
|
" \"You are a JSON generator. Your task is to read content and output ONLY a valid JSON array.\\n\"\n",
|
||||||
|
" \"Each object must have exactly two fields: 'instruction' and 'response'.\\n\"\n",
|
||||||
|
" \"Do not include any text before or after the JSON array.\\n\"\n",
|
||||||
|
" \"The instruction field should be a question or task from the content.\\n\"\n",
|
||||||
|
" \"The response field should be the answer extracted from the content.\\n\"\n",
|
||||||
|
" \"Output MUST be valid JSON - nothing else.\"\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" user_prompt = (\n",
|
||||||
|
" f\"Content to extract training pairs from:\\n{data_block}\\n\\n\"\n",
|
||||||
|
" \"Output a JSON array with instruction-response pairs. Output ONLY the JSON array, no other text:\"\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" prompt = f\"<|im_start|>system\\n{system_prompt}<|im_end|>\\n<|im_start|>user\\n{user_prompt}<|im_end|>\\n<|im_start|>assistant\\n[\"\n",
|
||||||
|
"\n",
|
||||||
|
" try:\n",
|
||||||
|
" inputs = tokenizer(prompt, return_tensors=\"pt\").to(model.device)\n",
|
||||||
|
"\n",
|
||||||
|
" if torch.cuda.is_available():\n",
|
||||||
|
" torch.cuda.empty_cache()\n",
|
||||||
|
"\n",
|
||||||
|
" with torch.no_grad():\n",
|
||||||
|
" output = model.generate(\n",
|
||||||
|
" **inputs,\n",
|
||||||
|
" max_new_tokens=max_tokens,\n",
|
||||||
|
" do_sample=True,\n",
|
||||||
|
" temperature=0.7,\n",
|
||||||
|
" top_p=0.95,\n",
|
||||||
|
" top_k=50,\n",
|
||||||
|
" eos_token_id=tokenizer.eos_token_id,\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" input_length = inputs.input_ids.shape[1]\n",
|
||||||
|
" generated_tokens = output[0][input_length:]\n",
|
||||||
|
" decoded = tokenizer.decode(generated_tokens, skip_special_tokens=True)\n",
|
||||||
|
"\n",
|
||||||
|
" if DEBUG_OUTPUT:\n",
|
||||||
|
" print(f\"\\n[BATCH {batch_num} RAW OUTPUT]\")\n",
|
||||||
|
" print(decoded[:500])\n",
|
||||||
|
" print(\"\\n---\")\n",
|
||||||
|
" logger.debug(f\"Model output (first 300 chars): {decoded[:300]}\")\n",
|
||||||
|
"\n",
|
||||||
|
" json_text = \"[\" + decoded\n",
|
||||||
|
"\n",
|
||||||
|
" json_start = json_text.find(\"[\")\n",
|
||||||
|
" if json_start == -1:\n",
|
||||||
|
" logger.warning(f\"No JSON array found in batch {batch_num} output\")\n",
|
||||||
|
" if DEBUG_OUTPUT:\n",
|
||||||
|
" print(f\"[BATCH {batch_num}] No '[' found in output\")\n",
|
||||||
|
" continue\n",
|
||||||
|
"\n",
|
||||||
|
" bracket_count = 0\n",
|
||||||
|
" in_string = False\n",
|
||||||
|
" escape_next = False\n",
|
||||||
|
" json_end = -1\n",
|
||||||
|
"\n",
|
||||||
|
" for idx in range(json_start, len(json_text)):\n",
|
||||||
|
" char = json_text[idx]\n",
|
||||||
|
"\n",
|
||||||
|
" if escape_next:\n",
|
||||||
|
" escape_next = False\n",
|
||||||
|
" continue\n",
|
||||||
|
"\n",
|
||||||
|
" if char == '\\\\':\n",
|
||||||
|
" escape_next = True\n",
|
||||||
|
" continue\n",
|
||||||
|
"\n",
|
||||||
|
" if char == '\"' and not escape_next:\n",
|
||||||
|
" in_string = not in_string\n",
|
||||||
|
" continue\n",
|
||||||
|
"\n",
|
||||||
|
" if not in_string:\n",
|
||||||
|
" if char == '[':\n",
|
||||||
|
" bracket_count += 1\n",
|
||||||
|
" elif char == ']':\n",
|
||||||
|
" bracket_count -= 1\n",
|
||||||
|
" if bracket_count == 0:\n",
|
||||||
|
" json_end = idx\n",
|
||||||
|
" break\n",
|
||||||
|
"\n",
|
||||||
|
" if json_end == -1:\n",
|
||||||
|
" logger.warning(f\"Failed to find JSON array boundary in batch {batch_num}\")\n",
|
||||||
|
" continue\n",
|
||||||
|
"\n",
|
||||||
|
" try:\n",
|
||||||
|
" json_text = json_text[json_start: json_end + 1]\n",
|
||||||
|
" parsed = json.loads(json_text)\n",
|
||||||
|
"\n",
|
||||||
|
" batch_pairs = 0\n",
|
||||||
|
" for item in parsed:\n",
|
||||||
|
" instr = str(item.get(\"instruction\", \"\")).strip()\n",
|
||||||
|
" resp = str(item.get(\"response\", \"\")).strip()\n",
|
||||||
|
" if instr and resp:\n",
|
||||||
|
" all_pairs.append((instr, resp))\n",
|
||||||
|
" if DEBUG_OUTPUT:\n",
|
||||||
|
" print(f\"Instruction: {instr}\\nResponse: {resp}\\n---\")\n",
|
||||||
|
" batch_pairs += 1\n",
|
||||||
|
"\n",
|
||||||
|
" logger.info(f\"Extracted {batch_pairs} pairs from batch {batch_num}\")\n",
|
||||||
|
" except json.JSONDecodeError as e:\n",
|
||||||
|
" logger.error(f\"Failed to parse JSON in batch {batch_num}: {str(e)}\")\n",
|
||||||
|
" if DEBUG_OUTPUT:\n",
|
||||||
|
" logger.debug(f\"JSON text attempted (first 500 chars): {json_text[:500]}\")\n",
|
||||||
|
"\n",
|
||||||
|
" try:\n",
|
||||||
|
" json_text_fixed = json_text.replace(',]', ']').replace(',}', '}')\n",
|
||||||
|
" parsed = json.loads(json_text_fixed)\n",
|
||||||
|
"\n",
|
||||||
|
" batch_pairs = 0\n",
|
||||||
|
" for item in parsed:\n",
|
||||||
|
" instr = str(item.get(\"instruction\", \"\")).strip()\n",
|
||||||
|
" resp = str(item.get(\"response\", \"\")).strip()\n",
|
||||||
|
" if instr and resp:\n",
|
||||||
|
" all_pairs.append((instr, resp))\n",
|
||||||
|
" if DEBUG_OUTPUT:\n",
|
||||||
|
" print(f\"Instruction: {instr}\\nResponse: {resp}\\n---\")\n",
|
||||||
|
" batch_pairs += 1\n",
|
||||||
|
"\n",
|
||||||
|
" logger.info(f\"Fixed JSON and extracted {batch_pairs} pairs from batch {batch_num}\")\n",
|
||||||
|
" except Exception as e2:\n",
|
||||||
|
" logger.error(f\"Could not fix JSON in batch {batch_num}: {str(e2)}\")\n",
|
||||||
|
" continue\n",
|
||||||
|
" except Exception as e:\n",
|
||||||
|
" logger.error(f\"Unexpected error parsing batch {batch_num}: {str(e)}\")\n",
|
||||||
|
" continue\n",
|
||||||
|
"\n",
|
||||||
|
" except RuntimeError as e:\n",
|
||||||
|
" if \"out of memory\" in str(e).lower():\n",
|
||||||
|
" logger.error(f\"OOM in batch {batch_num}. Try reducing batch_size or max_tokens.\")\n",
|
||||||
|
" if torch.cuda.is_available():\n",
|
||||||
|
" torch.cuda.empty_cache()\n",
|
||||||
|
" continue\n",
|
||||||
|
" raise\n",
|
||||||
|
"\n",
|
||||||
|
" logger.info(f\"Total pairs generated: {len(all_pairs)}\")\n",
|
||||||
|
" return all_pairs\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"training_pairs = generate_pairs_with_model(training_data, batch_size=None, max_tokens=2048)\n",
|
||||||
|
"logger.info(f\"Generated {len(training_pairs)} training pairs\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "85673dcd",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"print(f\"\\n{'='*80}\")\n",
|
||||||
|
"print(f\"Total training pairs generated: {len(training_pairs)}\")\n",
|
||||||
|
"print(f\"{'='*80}\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
"if training_pairs:\n",
|
||||||
|
" print(\"Sample training pairs:\")\n",
|
||||||
|
" for i, (instr, resp) in enumerate(training_pairs[:3], 1):\n",
|
||||||
|
" print(f\"\\nPair {i}:\")\n",
|
||||||
|
" print(f\" Instruction: {instr[:100]}{'...' if len(instr) > 100 else ''}\")\n",
|
||||||
|
" print(f\" Response: {resp[:100]}{'...' if len(resp) > 100 else ''}\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "4dec03c6",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Step 4: Save Training Data to JSONL Format\n",
|
||||||
|
"\n",
|
||||||
|
"Export the generated pairs to a JSONL file for use with fine-tuning pipelines."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "f0f727ee",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"output_file = DATA_DIR / \"generated_training_pairs.jsonl\"\n",
|
||||||
|
"\n",
|
||||||
|
"logger.info(f\"Saving {len(training_pairs)} pairs to {output_file}\")\n",
|
||||||
|
"\n",
|
||||||
|
"with open(output_file, 'w', encoding='utf-8') as f:\n",
|
||||||
|
" for instruction, response in training_pairs:\n",
|
||||||
|
" training_pair = {\n",
|
||||||
|
" \"instruction\": instruction,\n",
|
||||||
|
" \"output\": response,\n",
|
||||||
|
" }\n",
|
||||||
|
" f.write(json.dumps(training_pair, ensure_ascii=False) + \"\\n\")\n",
|
||||||
|
"\n",
|
||||||
|
"logger.info(f\"Training data saved to {output_file}\")\n",
|
||||||
|
"print(f\"\\n✓ Training pairs saved to: {output_file}\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "761f92c1",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Cleanup\n",
|
||||||
|
"\n",
|
||||||
|
"Free GPU memory after pair generation is complete."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "db644782",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"del model\n",
|
||||||
|
"del tokenizer\n",
|
||||||
|
"import gc\n",
|
||||||
|
"gc.collect()\n",
|
||||||
|
"\n",
|
||||||
|
"if torch.cuda.is_available():\n",
|
||||||
|
" torch.cuda.empty_cache()\n",
|
||||||
|
" torch.cuda.synchronize()\n",
|
||||||
|
"\n",
|
||||||
|
"logger.info(\"GPU memory freed\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": ".venv",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
393
notebooks/remote-agent-testing.ipynb
Normal file
393
notebooks/remote-agent-testing.ipynb
Normal file
|
|
@ -0,0 +1,393 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "5133f8fa",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Remote Agent Testing\n",
|
||||||
|
"Using google genAI to test an agentic workflow with Gemini 2.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"id": "62ec2147",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"True"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 1,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"# Imports\n",
|
||||||
|
"import os \n",
|
||||||
|
"from dotenv import load_dotenv\n",
|
||||||
|
"from langchain.agents import create_agent\n",
|
||||||
|
"from langchain.agents.middleware import dynamic_prompt, ModelRequest\n",
|
||||||
|
"from langchain.chat_models import init_chat_model\n",
|
||||||
|
"from langchain.tools import tool\n",
|
||||||
|
"from langchain_chroma import Chroma\n",
|
||||||
|
"from langchain_google_genai import GoogleGenerativeAIEmbeddings\n",
|
||||||
|
"from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
|
||||||
|
"\n",
|
||||||
|
"load_dotenv(os.path.join('', '..', '.env'))"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "6dc525a1",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"Using Gemini 2.5 via Langchain's Google Generative AI integration to test an agentic workflow."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"id": "a401cf8a",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"model = init_chat_model(\"google_genai:gemini-2.5-flash-lite\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "aaa68979",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"Setting up embeddings model"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"id": "45805907",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"embeddings = GoogleGenerativeAIEmbeddings(model=\"models/gemini-embedding-001\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "b3f90586",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"Vector store setup for data storage and retrieval"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 4,
|
||||||
|
"id": "500f90f4",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"vector_store = Chroma(\n",
|
||||||
|
" collection_name=\"example_collection\",\n",
|
||||||
|
" embedding_function=embeddings,\n",
|
||||||
|
" persist_directory=\"./build/langchain_db\",\n",
|
||||||
|
")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"id": "d4ff7ec0",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"6,900 pages later… *“This story is just for that one reader.”* \n",
|
||||||
|
"*Omniscient Reader’s Viewpoint* is probably one of the most ambitious epics I’ve ever read in this genre. Regression-themed novels are already a flooded trope, but this one blows the rest out of the water purely from how many layers it stacks on top of itself and still manages to come out narratively clean. When I first got into this series (via the webtoon, like most people), the wait between weekly releases drove me up the wall,\n",
|
||||||
|
"Total characters: 8578\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"import requests\n",
|
||||||
|
"from langchain_core.documents import Document\n",
|
||||||
|
"\n",
|
||||||
|
"response = requests.get(\"https://viswamedha.com/api/post/a-story-for-one-reader/\")\n",
|
||||||
|
"data = response.json()\n",
|
||||||
|
"content = data['content']\n",
|
||||||
|
"\n",
|
||||||
|
"docs = [Document(page_content=content, metadata={\"source\": response.url})]\n",
|
||||||
|
"\n",
|
||||||
|
"assert len(docs) == 1\n",
|
||||||
|
"print(docs[0].page_content[:500])\n",
|
||||||
|
"print(f\"Total characters: {len(docs[0].page_content)}\")\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 6,
|
||||||
|
"id": "82bcfabc",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Split blog post into 13 sub-documents.\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"text_splitter = RecursiveCharacterTextSplitter(\n",
|
||||||
|
" chunk_size=1000, \n",
|
||||||
|
" chunk_overlap=200, \n",
|
||||||
|
" add_start_index=True,\n",
|
||||||
|
")\n",
|
||||||
|
"all_splits = text_splitter.split_documents(docs)\n",
|
||||||
|
"\n",
|
||||||
|
"print(f\"Split blog post into {len(all_splits)} sub-documents.\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"id": "2ee1a9ca",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"['44706f38-6bd0-4e9a-8a41-d27790bdddc8', '9d2a2300-a311-4389-86b8-71eef221186c', '3970098b-f681-47bb-8c1d-6929cb67b537']\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"document_ids = vector_store.add_documents(documents=all_splits)\n",
|
||||||
|
"\n",
|
||||||
|
"print(document_ids[:3])"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 8,
|
||||||
|
"id": "a9096893",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@tool(response_format=\"content_and_artifact\")\n",
|
||||||
|
"def retrieve_context(query: str):\n",
|
||||||
|
" \"\"\"Retrieve information to help answer a query.\"\"\"\n",
|
||||||
|
" retrieved_docs = vector_store.similarity_search(query, k=2)\n",
|
||||||
|
" serialized = \"\\n\\n\".join(\n",
|
||||||
|
" (f\"Source: {doc.metadata}\\nContent: {doc.page_content}\")\n",
|
||||||
|
" for doc in retrieved_docs\n",
|
||||||
|
" )\n",
|
||||||
|
" return serialized, retrieved_docs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 9,
|
||||||
|
"id": "dff2345d",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"tools = [retrieve_context]\n",
|
||||||
|
"prompt = (\n",
|
||||||
|
" \"You have access to a tool that retrieves context from a blog post. \"\n",
|
||||||
|
" \"Use the tool to help answer user queries.\"\n",
|
||||||
|
")\n",
|
||||||
|
"agent = create_agent(model, tools, system_prompt=prompt)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 10,
|
||||||
|
"id": "aaa2fad9",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"================================\u001b[1m Human Message \u001b[0m=================================\n",
|
||||||
|
"\n",
|
||||||
|
"What is the significance of the second loop?\n",
|
||||||
|
"\n",
|
||||||
|
"Use the retrieved context to provide a detailed answer.\n",
|
||||||
|
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||||
|
"Tool Calls:\n",
|
||||||
|
" retrieve_context (b6c5ce4e-a030-47cf-8fed-f1279f022766)\n",
|
||||||
|
" Call ID: b6c5ce4e-a030-47cf-8fed-f1279f022766\n",
|
||||||
|
" Args:\n",
|
||||||
|
" query: Significance of the second loop\n",
|
||||||
|
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
|
||||||
|
"Name: retrieve_context\n",
|
||||||
|
"\n",
|
||||||
|
"Source: {'start_index': 3377, 'source': 'https://viswamedha.com/api/post/a-story-for-one-reader/'}\n",
|
||||||
|
"Content: And this is where the paradox really hits. The Great Plotter, while observing regressions and chasing a better ending, ends up **creating the very timeline** he’s been watching. In trying to fix his own story, he triggers a new one. He unknowingly causes the very events that lead to KDJ’s worldline existing in the first place. It's absolutely wild. He becomes the most influential figure in this timeline, yet completely powerless to interact with it directly (due to the constraints of Probability). All he can do is watch as KDJ lives through the story he thought he already knew.\n",
|
||||||
|
"\n",
|
||||||
|
"---\n",
|
||||||
|
"\n",
|
||||||
|
"## What is the second paradox, and where does the loop begin?\n",
|
||||||
|
"\n",
|
||||||
|
"Source: {'start_index': 3377, 'source': 'https://viswamedha.com/api/post/a-story-for-one-reader/'}\n",
|
||||||
|
"Content: And this is where the paradox really hits. The Great Plotter, while observing regressions and chasing a better ending, ends up **creating the very timeline** he’s been watching. In trying to fix his own story, he triggers a new one. He unknowingly causes the very events that lead to KDJ’s worldline existing in the first place. It's absolutely wild. He becomes the most influential figure in this timeline, yet completely powerless to interact with it directly (due to the constraints of Probability). All he can do is watch as KDJ lives through the story he thought he already knew.\n",
|
||||||
|
"\n",
|
||||||
|
"---\n",
|
||||||
|
"\n",
|
||||||
|
"## What is the second paradox, and where does the loop begin?\n",
|
||||||
|
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
|
||||||
|
"\n",
|
||||||
|
"The second loop is significant because it represents a paradox where the Great Plotter, in his attempt to alter his own story and create a better ending, inadvertently becomes the catalyst for the very timeline he is observing. He ends up creating the timeline he has been watching, triggering new events, and causing the existence of KDJ's worldline. Despite being the most influential figure in this new timeline, the Great Plotter is powerless to intervene directly and can only watch as KDJ experiences the story.\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"query = (\n",
|
||||||
|
" \"What is the significance of the second loop?\\n\\n\"\n",
|
||||||
|
" \"Use the retrieved context to provide a detailed answer.\"\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"for event in agent.stream(\n",
|
||||||
|
" {\"messages\": [{\"role\": \"user\", \"content\": query}]},\n",
|
||||||
|
" stream_mode=\"values\",\n",
|
||||||
|
"):\n",
|
||||||
|
" event[\"messages\"][-1].pretty_print()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 11,
|
||||||
|
"id": "bda6d7d0",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@dynamic_prompt\n",
|
||||||
|
"def prompt_with_context(request: ModelRequest) -> str:\n",
|
||||||
|
" \"\"\"Inject context into state messages.\"\"\"\n",
|
||||||
|
" last_query = request.state[\"messages\"][-1].text\n",
|
||||||
|
" retrieved_docs = vector_store.similarity_search(last_query)\n",
|
||||||
|
"\n",
|
||||||
|
" docs_content = \"\\n\\n\".join(doc.page_content for doc in retrieved_docs)\n",
|
||||||
|
"\n",
|
||||||
|
" system_message = (\n",
|
||||||
|
" \"You are a helpful assistant. Use the following context in your response:\"\n",
|
||||||
|
" f\"\\n\\n{docs_content}\"\n",
|
||||||
|
" )\n",
|
||||||
|
"\n",
|
||||||
|
" return system_message\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"agent = create_agent(model, tools=[], middleware=[prompt_with_context])"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "1540855c",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"{'messages': [HumanMessage(content='What is the significance of the second loop?\\n\\n', additional_kwargs={}, response_metadata={}, id='eaca10e5-a350-4ad8-80ad-c62645b69e5a')]}\n",
|
||||||
|
"================================\u001b[1m Human Message \u001b[0m=================================\n",
|
||||||
|
"\n",
|
||||||
|
"What is the significance of the second loop?\n",
|
||||||
|
"\n",
|
||||||
|
"\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ename": "GoogleGenerativeAIError",
|
||||||
|
"evalue": "Error embedding content: 500 INTERNAL. {'error': {'code': 500, 'message': 'Internal error encountered.', 'status': 'INTERNAL'}}",
|
||||||
|
"output_type": "error",
|
||||||
|
"traceback": [
|
||||||
|
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
||||||
|
"\u001b[31mServerError\u001b[39m Traceback (most recent call last)",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langchain_google_genai\\embeddings.py:480\u001b[39m, in \u001b[36mGoogleGenerativeAIEmbeddings.embed_query\u001b[39m\u001b[34m(self, text, task_type, title, output_dimensionality)\u001b[39m\n\u001b[32m 479\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m480\u001b[39m result = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mclient\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmodels\u001b[49m\u001b[43m.\u001b[49m\u001b[43membed_content\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 481\u001b[39m \u001b[43m \u001b[49m\u001b[43mmodel\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mmodel\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 482\u001b[39m \u001b[43m \u001b[49m\u001b[43mcontents\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtext\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 483\u001b[39m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 484\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 485\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m ClientError \u001b[38;5;28;01mas\u001b[39;00m e:\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\google\\genai\\models.py:4179\u001b[39m, in \u001b[36mModels.embed_content\u001b[39m\u001b[34m(self, model, contents, config)\u001b[39m\n\u001b[32m 4177\u001b[39m request_dict = _common.encode_unserializable_types(request_dict)\n\u001b[32m-> \u001b[39m\u001b[32m4179\u001b[39m response = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_api_client\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 4180\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mpost\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrequest_dict\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mhttp_options\u001b[49m\n\u001b[32m 4181\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 4183\u001b[39m response_dict = {} \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m response.body \u001b[38;5;28;01melse\u001b[39;00m json.loads(response.body)\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\google\\genai\\_api_client.py:1386\u001b[39m, in \u001b[36mBaseApiClient.request\u001b[39m\u001b[34m(self, http_method, path, request_dict, http_options)\u001b[39m\n\u001b[32m 1383\u001b[39m http_request = \u001b[38;5;28mself\u001b[39m._build_request(\n\u001b[32m 1384\u001b[39m http_method, path, request_dict, http_options\n\u001b[32m 1385\u001b[39m )\n\u001b[32m-> \u001b[39m\u001b[32m1386\u001b[39m response = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_request\u001b[49m\u001b[43m(\u001b[49m\u001b[43mhttp_request\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mhttp_options\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[32m 1387\u001b[39m response_body = (\n\u001b[32m 1388\u001b[39m response.response_stream[\u001b[32m0\u001b[39m] \u001b[38;5;28;01mif\u001b[39;00m response.response_stream \u001b[38;5;28;01melse\u001b[39;00m \u001b[33m'\u001b[39m\u001b[33m'\u001b[39m\n\u001b[32m 1389\u001b[39m )\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\google\\genai\\_api_client.py:1222\u001b[39m, in \u001b[36mBaseApiClient._request\u001b[39m\u001b[34m(self, http_request, http_options, stream)\u001b[39m\n\u001b[32m 1220\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m retry(\u001b[38;5;28mself\u001b[39m._request_once, http_request, stream) \u001b[38;5;66;03m# type: ignore[no-any-return]\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1222\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_retry\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_request_once\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mhttp_request\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m)\u001b[49m\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\tenacity\\__init__.py:477\u001b[39m, in \u001b[36mRetrying.__call__\u001b[39m\u001b[34m(self, fn, *args, **kwargs)\u001b[39m\n\u001b[32m 476\u001b[39m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m477\u001b[39m do = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43miter\u001b[49m\u001b[43m(\u001b[49m\u001b[43mretry_state\u001b[49m\u001b[43m=\u001b[49m\u001b[43mretry_state\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 478\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(do, DoAttempt):\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\tenacity\\__init__.py:378\u001b[39m, in \u001b[36mBaseRetrying.iter\u001b[39m\u001b[34m(self, retry_state)\u001b[39m\n\u001b[32m 377\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m action \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m.iter_state.actions:\n\u001b[32m--> \u001b[39m\u001b[32m378\u001b[39m result = \u001b[43maction\u001b[49m\u001b[43m(\u001b[49m\u001b[43mretry_state\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 379\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m result\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\tenacity\\__init__.py:420\u001b[39m, in \u001b[36mBaseRetrying._post_stop_check_actions.<locals>.exc_check\u001b[39m\u001b[34m(rs)\u001b[39m\n\u001b[32m 419\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.reraise:\n\u001b[32m--> \u001b[39m\u001b[32m420\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[43mretry_exc\u001b[49m\u001b[43m.\u001b[49m\u001b[43mreraise\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 421\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m retry_exc \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mfut\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mexception\u001b[39;00m()\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\tenacity\\__init__.py:187\u001b[39m, in \u001b[36mRetryError.reraise\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 186\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.last_attempt.failed:\n\u001b[32m--> \u001b[39m\u001b[32m187\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mlast_attempt\u001b[49m\u001b[43m.\u001b[49m\u001b[43mresult\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 188\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;28mself\u001b[39m\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mC:\\Program Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.2544.0_x64__qbz5n2kfra8p0\\Lib\\concurrent\\futures\\_base.py:449\u001b[39m, in \u001b[36mFuture.result\u001b[39m\u001b[34m(self, timeout)\u001b[39m\n\u001b[32m 448\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._state == FINISHED:\n\u001b[32m--> \u001b[39m\u001b[32m449\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m__get_result\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 451\u001b[39m \u001b[38;5;28mself\u001b[39m._condition.wait(timeout)\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mC:\\Program Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.2544.0_x64__qbz5n2kfra8p0\\Lib\\concurrent\\futures\\_base.py:401\u001b[39m, in \u001b[36mFuture.__get_result\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 400\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m401\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;28mself\u001b[39m._exception\n\u001b[32m 402\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[32m 403\u001b[39m \u001b[38;5;66;03m# Break a reference cycle with the exception in self._exception\u001b[39;00m\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\tenacity\\__init__.py:480\u001b[39m, in \u001b[36mRetrying.__call__\u001b[39m\u001b[34m(self, fn, *args, **kwargs)\u001b[39m\n\u001b[32m 479\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m480\u001b[39m result = \u001b[43mfn\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 481\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m: \u001b[38;5;66;03m# noqa: B902\u001b[39;00m\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\google\\genai\\_api_client.py:1199\u001b[39m, in \u001b[36mBaseApiClient._request_once\u001b[39m\u001b[34m(self, http_request, stream)\u001b[39m\n\u001b[32m 1192\u001b[39m response = \u001b[38;5;28mself\u001b[39m._httpx_client.request(\n\u001b[32m 1193\u001b[39m method=http_request.method,\n\u001b[32m 1194\u001b[39m url=http_request.url,\n\u001b[32m (...)\u001b[39m\u001b[32m 1197\u001b[39m timeout=http_request.timeout,\n\u001b[32m 1198\u001b[39m )\n\u001b[32m-> \u001b[39m\u001b[32m1199\u001b[39m \u001b[43merrors\u001b[49m\u001b[43m.\u001b[49m\u001b[43mAPIError\u001b[49m\u001b[43m.\u001b[49m\u001b[43mraise_for_response\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresponse\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1200\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m HttpResponse(\n\u001b[32m 1201\u001b[39m response.headers, response \u001b[38;5;28;01mif\u001b[39;00m stream \u001b[38;5;28;01melse\u001b[39;00m [response.text]\n\u001b[32m 1202\u001b[39m )\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\google\\genai\\errors.py:121\u001b[39m, in \u001b[36mAPIError.raise_for_response\u001b[39m\u001b[34m(cls, response)\u001b[39m\n\u001b[32m 119\u001b[39m response_json = response.body_segments[\u001b[32m0\u001b[39m].get(\u001b[33m'\u001b[39m\u001b[33merror\u001b[39m\u001b[33m'\u001b[39m, {})\n\u001b[32m--> \u001b[39m\u001b[32m121\u001b[39m \u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mraise_error\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresponse\u001b[49m\u001b[43m.\u001b[49m\u001b[43mstatus_code\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mresponse_json\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mresponse\u001b[49m\u001b[43m)\u001b[49m\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\google\\genai\\errors.py:148\u001b[39m, in \u001b[36mAPIError.raise_error\u001b[39m\u001b[34m(cls, status_code, response_json, response)\u001b[39m\n\u001b[32m 147\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[32m500\u001b[39m <= status_code < \u001b[32m600\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m148\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m ServerError(status_code, response_json, response)\n\u001b[32m 149\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n",
|
||||||
|
"\u001b[31mServerError\u001b[39m: 500 INTERNAL. {'error': {'code': 500, 'message': 'Internal error encountered.', 'status': 'INTERNAL'}}",
|
||||||
|
"\nThe above exception was the direct cause of the following exception:\n",
|
||||||
|
"\u001b[31mGoogleGenerativeAIError\u001b[39m Traceback (most recent call last)",
|
||||||
|
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[13]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstep\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43magent\u001b[49m\u001b[43m.\u001b[49m\u001b[43mstream\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2\u001b[39m \u001b[43m \u001b[49m\u001b[43m{\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmessages\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43m{\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrole\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43muser\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcontent\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mquery\u001b[49m\u001b[43m}\u001b[49m\u001b[43m]\u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mvalues\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 4\u001b[39m \u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 5\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mimport\u001b[39;49;00m\u001b[38;5;250;43m \u001b[39;49m\u001b[34;43;01mpprint\u001b[39;49;00m\n\u001b[32m 6\u001b[39m \u001b[43m \u001b[49m\u001b[43mpprint\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpprint\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstep\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# inspect the event structure\u001b[39;49;00m\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langgraph\\pregel\\main.py:2646\u001b[39m, in \u001b[36mPregel.stream\u001b[39m\u001b[34m(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, subgraphs, debug, **kwargs)\u001b[39m\n\u001b[32m 2644\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m task \u001b[38;5;129;01min\u001b[39;00m loop.match_cached_writes():\n\u001b[32m 2645\u001b[39m loop.output_writes(task.id, task.writes, cached=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m-> \u001b[39m\u001b[32m2646\u001b[39m \u001b[43m\u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mrunner\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtick\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2647\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mt\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mloop\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtasks\u001b[49m\u001b[43m.\u001b[49m\u001b[43mvalues\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m.\u001b[49m\u001b[43mwrites\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2648\u001b[39m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mstep_timeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2649\u001b[39m \u001b[43m \u001b[49m\u001b[43mget_waiter\u001b[49m\u001b[43m=\u001b[49m\u001b[43mget_waiter\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2650\u001b[39m \u001b[43m \u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m=\u001b[49m\u001b[43mloop\u001b[49m\u001b[43m.\u001b[49m\u001b[43maccept_push\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2651\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 2652\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# emit output\u001b[39;49;00m\n\u001b[32m 2653\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01myield from\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_output\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2654\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mprint_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msubgraphs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mqueue\u001b[49m\u001b[43m.\u001b[49m\u001b[43mEmpty\u001b[49m\n\u001b[32m 2655\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 2656\u001b[39m loop.after_tick()\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langgraph\\pregel\\_runner.py:167\u001b[39m, in \u001b[36mPregelRunner.tick\u001b[39m\u001b[34m(self, tasks, reraise, timeout, retry_policy, get_waiter, schedule_task)\u001b[39m\n\u001b[32m 165\u001b[39m t = tasks[\u001b[32m0\u001b[39m]\n\u001b[32m 166\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m167\u001b[39m \u001b[43mrun_with_retry\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 168\u001b[39m \u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 169\u001b[39m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 170\u001b[39m \u001b[43m \u001b[49m\u001b[43mconfigurable\u001b[49m\u001b[43m=\u001b[49m\u001b[43m{\u001b[49m\n\u001b[32m 171\u001b[39m \u001b[43m \u001b[49m\u001b[43mCONFIG_KEY_CALL\u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mpartial\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 172\u001b[39m \u001b[43m \u001b[49m\u001b[43m_call\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 173\u001b[39m \u001b[43m \u001b[49m\u001b[43mweakref\u001b[49m\u001b[43m.\u001b[49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mt\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 174\u001b[39m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m=\u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 175\u001b[39m \u001b[43m \u001b[49m\u001b[43mfutures\u001b[49m\u001b[43m=\u001b[49m\u001b[43mweakref\u001b[49m\u001b[43m.\u001b[49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfutures\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 176\u001b[39m \u001b[43m \u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m=\u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 177\u001b[39m \u001b[43m \u001b[49m\u001b[43msubmit\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msubmit\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 178\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 179\u001b[39m \u001b[43m \u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 180\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 181\u001b[39m \u001b[38;5;28mself\u001b[39m.commit(t, \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m 182\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langgraph\\pregel\\_retry.py:42\u001b[39m, in \u001b[36mrun_with_retry\u001b[39m\u001b[34m(task, retry_policy, configurable)\u001b[39m\n\u001b[32m 40\u001b[39m task.writes.clear()\n\u001b[32m 41\u001b[39m \u001b[38;5;66;03m# run the task\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m42\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mtask\u001b[49m\u001b[43m.\u001b[49m\u001b[43mproc\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtask\u001b[49m\u001b[43m.\u001b[49m\u001b[43minput\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 43\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m ParentCommand \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m 44\u001b[39m ns: \u001b[38;5;28mstr\u001b[39m = config[CONF][CONFIG_KEY_CHECKPOINT_NS]\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langgraph\\_internal\\_runnable.py:656\u001b[39m, in \u001b[36mRunnableSeq.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 654\u001b[39m \u001b[38;5;66;03m# run in context\u001b[39;00m\n\u001b[32m 655\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m set_config_context(config, run) \u001b[38;5;28;01mas\u001b[39;00m context:\n\u001b[32m--> \u001b[39m\u001b[32m656\u001b[39m \u001b[38;5;28minput\u001b[39m = \u001b[43mcontext\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstep\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 657\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 658\u001b[39m \u001b[38;5;28minput\u001b[39m = step.invoke(\u001b[38;5;28minput\u001b[39m, config)\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langgraph\\_internal\\_runnable.py:400\u001b[39m, in \u001b[36mRunnableCallable.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 398\u001b[39m run_manager.on_chain_end(ret)\n\u001b[32m 399\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m400\u001b[39m ret = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 401\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.recurse \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(ret, Runnable):\n\u001b[32m 402\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m ret.invoke(\u001b[38;5;28minput\u001b[39m, config)\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langchain\\agents\\factory.py:1144\u001b[39m, in \u001b[36mcreate_agent.<locals>.model_node\u001b[39m\u001b[34m(state, runtime)\u001b[39m\n\u001b[32m 1141\u001b[39m response = _execute_model_sync(request)\n\u001b[32m 1142\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 1143\u001b[39m \u001b[38;5;66;03m# Call composed handler with base handler\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1144\u001b[39m response = \u001b[43mwrap_model_call_handler\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m_execute_model_sync\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1146\u001b[39m \u001b[38;5;66;03m# Extract state updates from ModelResponse\u001b[39;00m\n\u001b[32m 1147\u001b[39m state_updates = {\u001b[33m\"\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m\"\u001b[39m: response.result}\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langchain\\agents\\factory.py:146\u001b[39m, in \u001b[36m_chain_model_call_handlers.<locals>.normalized_single\u001b[39m\u001b[34m(request, handler)\u001b[39m\n\u001b[32m 142\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mnormalized_single\u001b[39m(\n\u001b[32m 143\u001b[39m request: ModelRequest,\n\u001b[32m 144\u001b[39m handler: Callable[[ModelRequest], ModelResponse],\n\u001b[32m 145\u001b[39m ) -> ModelResponse:\n\u001b[32m--> \u001b[39m\u001b[32m146\u001b[39m result = \u001b[43msingle_handler\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mhandler\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 147\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m _normalize_to_model_response(result)\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langchain\\agents\\middleware\\types.py:1656\u001b[39m, in \u001b[36mdynamic_prompt.<locals>.decorator.<locals>.wrapped\u001b[39m\u001b[34m(_self, request, handler)\u001b[39m\n\u001b[32m 1651\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mwrapped\u001b[39m(\n\u001b[32m 1652\u001b[39m _self: AgentMiddleware[StateT, ContextT],\n\u001b[32m 1653\u001b[39m request: ModelRequest,\n\u001b[32m 1654\u001b[39m handler: Callable[[ModelRequest], ModelResponse],\n\u001b[32m 1655\u001b[39m ) -> ModelCallResult:\n\u001b[32m-> \u001b[39m\u001b[32m1656\u001b[39m prompt = \u001b[43mcast\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mCallable[[ModelRequest], SystemMessage | str]\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m)\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1657\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(prompt, SystemMessage):\n\u001b[32m 1658\u001b[39m request = request.override(system_message=prompt)\n",
|
||||||
|
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 5\u001b[39m, in \u001b[36mprompt_with_context\u001b[39m\u001b[34m(request)\u001b[39m\n\u001b[32m 3\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Inject context into state messages.\"\"\"\u001b[39;00m\n\u001b[32m 4\u001b[39m last_query = request.state[\u001b[33m\"\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m\"\u001b[39m][-\u001b[32m1\u001b[39m].text\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m retrieved_docs = \u001b[43mvector_store\u001b[49m\u001b[43m.\u001b[49m\u001b[43msimilarity_search\u001b[49m\u001b[43m(\u001b[49m\u001b[43mlast_query\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 7\u001b[39m docs_content = \u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m\"\u001b[39m.join(doc.page_content \u001b[38;5;28;01mfor\u001b[39;00m doc \u001b[38;5;129;01min\u001b[39;00m retrieved_docs)\n\u001b[32m 9\u001b[39m system_message = (\n\u001b[32m 10\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mYou are a helpful assistant. Use the following context in your response:\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 11\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;132;01m{\u001b[39;00mdocs_content\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 12\u001b[39m )\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langchain_chroma\\vectorstores.py:748\u001b[39m, in \u001b[36mChroma.similarity_search\u001b[39m\u001b[34m(self, query, k, filter, **kwargs)\u001b[39m\n\u001b[32m 730\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34msimilarity_search\u001b[39m(\n\u001b[32m 731\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m 732\u001b[39m query: \u001b[38;5;28mstr\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 735\u001b[39m **kwargs: Any,\n\u001b[32m 736\u001b[39m ) -> \u001b[38;5;28mlist\u001b[39m[Document]:\n\u001b[32m 737\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Run similarity search with Chroma.\u001b[39;00m\n\u001b[32m 738\u001b[39m \n\u001b[32m 739\u001b[39m \u001b[33;03m Args:\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 746\u001b[39m \u001b[33;03m List of documents most similar to the query text.\u001b[39;00m\n\u001b[32m 747\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m748\u001b[39m docs_and_scores = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msimilarity_search_with_score\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 749\u001b[39m \u001b[43m \u001b[49m\u001b[43mquery\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 750\u001b[39m \u001b[43m \u001b[49m\u001b[43mk\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 751\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mfilter\u001b[39;49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mfilter\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 752\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 753\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 754\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m [doc \u001b[38;5;28;01mfor\u001b[39;00m doc, _ \u001b[38;5;129;01min\u001b[39;00m docs_and_scores]\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langchain_chroma\\vectorstores.py:848\u001b[39m, in \u001b[36mChroma.similarity_search_with_score\u001b[39m\u001b[34m(self, query, k, filter, where_document, **kwargs)\u001b[39m\n\u001b[32m 840\u001b[39m results = \u001b[38;5;28mself\u001b[39m.__query_collection(\n\u001b[32m 841\u001b[39m query_texts=[query],\n\u001b[32m 842\u001b[39m n_results=k,\n\u001b[32m (...)\u001b[39m\u001b[32m 845\u001b[39m **kwargs,\n\u001b[32m 846\u001b[39m )\n\u001b[32m 847\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m848\u001b[39m query_embedding = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_embedding_function\u001b[49m\u001b[43m.\u001b[49m\u001b[43membed_query\u001b[49m\u001b[43m(\u001b[49m\u001b[43mquery\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 849\u001b[39m results = \u001b[38;5;28mself\u001b[39m.__query_collection(\n\u001b[32m 850\u001b[39m query_embeddings=[query_embedding],\n\u001b[32m 851\u001b[39m n_results=k,\n\u001b[32m (...)\u001b[39m\u001b[32m 854\u001b[39m **kwargs,\n\u001b[32m 855\u001b[39m )\n\u001b[32m 857\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m _results_to_docs_and_scores(results)\n",
|
||||||
|
"\u001b[36mFile \u001b[39m\u001b[32mc:\\Users\\nalab\\University\\vxn217\\.venv\\Lib\\site-packages\\langchain_google_genai\\embeddings.py:490\u001b[39m, in \u001b[36mGoogleGenerativeAIEmbeddings.embed_query\u001b[39m\u001b[34m(self, text, task_type, title, output_dimensionality)\u001b[39m\n\u001b[32m 488\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 489\u001b[39m msg = \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mError embedding content: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00me\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m--> \u001b[39m\u001b[32m490\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m GoogleGenerativeAIError(msg) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01me\u001b[39;00m\n\u001b[32m 492\u001b[39m \u001b[38;5;66;03m# Single text returns single embedding\u001b[39;00m\n\u001b[32m 493\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mlist\u001b[39m(result.embeddings[\u001b[32m0\u001b[39m].values)\n",
|
||||||
|
"\u001b[31mGoogleGenerativeAIError\u001b[39m: Error embedding content: 500 INTERNAL. {'error': {'code': 500, 'message': 'Internal error encountered.', 'status': 'INTERNAL'}}",
|
||||||
|
"During task with name 'model' and id '2df4c75f-65ba-cd3e-b448-0ed95a7614f8'"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"query = \"What is the significance of the second loop?\\n\\n\"\n",
|
||||||
|
"for step in agent.stream(\n",
|
||||||
|
" {\"messages\": [{\"role\": \"user\", \"content\": query}]},\n",
|
||||||
|
" stream_mode=\"values\",\n",
|
||||||
|
"):\n",
|
||||||
|
" step[\"messages\"][-1].pretty_print()"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": ".venv",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
62
package-lock.json
generated
Normal file
62
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"name": "FYPtest",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"marked": "^17.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
|
"@types/marked": "^5.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/dompurify": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/trusted-types": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/marked": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "17.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.3.tgz",
|
||||||
|
"integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
package.json
Normal file
10
package.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"marked": "^17.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
|
"@types/marked": "^5.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
requirements/django.txt
Normal file
16
requirements/django.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
celery[redis]==5.6.2
|
||||||
|
channels==4.3.2
|
||||||
|
channels-redis==4.3.0
|
||||||
|
daphne==4.2.1
|
||||||
|
django==6.0.2
|
||||||
|
django-celery-results==2.6.0
|
||||||
|
django-cors-headers==4.9.0
|
||||||
|
djangorestframework==3.16.1
|
||||||
|
django-unfold==0.81.0
|
||||||
|
httpx==0.28.1
|
||||||
|
pgvector==0.4.2
|
||||||
|
psycopg2-binary==2.9.10
|
||||||
|
pypdf==5.4.0
|
||||||
|
python-docx==1.2.0
|
||||||
|
python-dotenv==1.2.1
|
||||||
|
whitenoise==6.11.0
|
||||||
8
requirements/inference.txt
Normal file
8
requirements/inference.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
--extra-index-url https://download.pytorch.org/whl/cu130
|
||||||
|
einops==0.8.0
|
||||||
|
fastapi[standard]==0.133.0
|
||||||
|
requests==2.32.5
|
||||||
|
sentence-transformers==5.2.0
|
||||||
|
torch==2.9.1+cu130
|
||||||
|
torchaudio==2.9.1+cu130
|
||||||
|
torchvision==0.24.1+cu130
|
||||||
5
site/.prettierignore
Normal file
5
site/.prettierignore
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Ignore docker-compose and other compose YAML files from Prettier formatting
|
||||||
|
docker-compose*.yml
|
||||||
|
docker-compose*.yaml
|
||||||
|
compose/**/*.yml
|
||||||
|
compose/**/*.yaml
|
||||||
9
site/.prettierrc.json
Normal file
9
site/.prettierrc.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"htmlWhitespaceSensitivity": "ignore"
|
||||||
|
}
|
||||||
12
site/env.d.ts
vendored
Normal file
12
site/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<
|
||||||
|
Record<string, unknown>,
|
||||||
|
Record<string, unknown>,
|
||||||
|
Record<string, unknown>,
|
||||||
|
Record<string, unknown>,
|
||||||
|
Record<string, unknown>
|
||||||
|
>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
31
site/eslint.config.ts
Normal file
31
site/eslint.config.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{vue,ts,mts,tsx}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
...pluginVue.configs['flat/essential'],
|
||||||
|
vueTsConfigs.recommended,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
indent: ['error', 4, { SwitchCase: 1 }],
|
||||||
|
'vue/html-indent': ['error', 4],
|
||||||
|
'vue/script-indent': ['error', 4, { baseIndent: 1, switchCase: 1 }],
|
||||||
|
'vue/block-lang': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
skipFormatting,
|
||||||
|
)
|
||||||
16
site/index.html
Normal file
16
site/index.html
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dynavera</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5915
site/package-lock.json
generated
Normal file
5915
site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
50
site/package.json
Normal file
50
site/package.json
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"name": "dynavera",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite -- --host",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"watch": "vite build --watch",
|
||||||
|
"devwatch": "concurrently \"npm run dev\" \"npm run watch\"",
|
||||||
|
"type-check": "vue-tsc --build",
|
||||||
|
"lint": "eslint . --fix --cache",
|
||||||
|
"format": "prettier --write --experimental-cli src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ant-design-vue": "^4.2.6",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
|
"marked": "^17.0.3",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.26",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node24": "^24.0.3",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
|
"@types/marked": "^5.0.2",
|
||||||
|
"@types/node": "^24.10.4",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.3",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^5.1.3",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"concurrently": "^8.2.0",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-plugin-vue": "~10.6.2",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
|
"npm-run-all2": "^8.0.4",
|
||||||
|
"prettier": "3.7.4",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^7.3.0",
|
||||||
|
"vite-plugin-vue-devtools": "^8.0.5",
|
||||||
|
"vue-tsc": "^3.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
331
site/src/App.vue
Normal file
331
site/src/App.vue
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, type Component } from 'vue'
|
||||||
|
import { Layout, Menu, Button, Space, Typography, Select } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
HomeOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
RobotOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
LoginOutlined,
|
||||||
|
UserAddOutlined,
|
||||||
|
BuildOutlined,
|
||||||
|
PayCircleOutlined,
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from './stores/userStore'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
type NavItem = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
icon: Component
|
||||||
|
path?: string
|
||||||
|
manager?: boolean
|
||||||
|
children?: NavItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const navItems: NavItem[] = [
|
||||||
|
{ key: '/', label: 'Home', icon: HomeOutlined, path: '/' },
|
||||||
|
{ key: '/about', label: 'About', icon: InfoCircleOutlined, path: '/about' },
|
||||||
|
{ key: '/pricing', label: 'Pricing', icon: PayCircleOutlined, path: '/pricing' },
|
||||||
|
{ key: '/agents', label: 'Agents', icon: RobotOutlined, path: '/agents', manager: true },
|
||||||
|
{ key: '/organization', label: 'Organizations', icon: BuildOutlined, path: '/organization' },
|
||||||
|
{ key: '/progress', label: 'Progress', icon: DashboardOutlined, path: '/progress' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const visibleNavItems = computed<NavItem[]>(() =>
|
||||||
|
navItems.filter((item) => (item.manager ? userStore.user?.is_manager : true)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedKeys = computed(() => {
|
||||||
|
for (const item of visibleNavItems.value) {
|
||||||
|
if (item.key === '/' && route.path === '/') return [item.key]
|
||||||
|
if (route.path.startsWith(item.key)) return [item.key]
|
||||||
|
if (item.children) {
|
||||||
|
const childMatch = item.children.find((c) => route.path.startsWith(c.key))
|
||||||
|
if (childMatch) return [item.key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
type SimpleMenuInfo = { key: string | number | Array<string | number> }
|
||||||
|
|
||||||
|
const onSelect = (info: SimpleMenuInfo) => {
|
||||||
|
const key = String(info.key)
|
||||||
|
let found: NavItem | undefined
|
||||||
|
for (const item of visibleNavItems.value) {
|
||||||
|
if (item.key === key) {
|
||||||
|
found = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (item.children) {
|
||||||
|
const child = item.children.find((c) => c.key === key)
|
||||||
|
if (child) {
|
||||||
|
found = child
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found && found.path && route.path !== found.path) {
|
||||||
|
const selectedOrgUuid = userStore.userSelectedOrganization?.uuid
|
||||||
|
if (found.path === '/organization' && selectedOrgUuid) {
|
||||||
|
router.push(`/organization/${selectedOrgUuid}`)
|
||||||
|
} else {
|
||||||
|
router.push(found.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await userStore.logout()
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
userStore.fetchSession()
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = userStore
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Layout class="shell">
|
||||||
|
<Layout.Header class="shell-header">
|
||||||
|
<div class="brand" @click="route.path !== '/' && router.push('/')">Dynavera</div>
|
||||||
|
<div style="margin-right: 1rem" v-if="user.isAuthenticated"></div>
|
||||||
|
<Menu
|
||||||
|
mode="horizontal"
|
||||||
|
theme="dark"
|
||||||
|
:selectedKeys="selectedKeys"
|
||||||
|
class="shell-menu"
|
||||||
|
@select="onSelect"
|
||||||
|
>
|
||||||
|
<template v-for="item in visibleNavItems" :key="item.key">
|
||||||
|
<Menu.SubMenu v-if="item.children" :key="`${item.key}-submenu`">
|
||||||
|
<template #title>
|
||||||
|
<span
|
||||||
|
@click.stop="
|
||||||
|
item.path && route.path !== item.path && router.push(item.path)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Space size="small">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</Space>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<Menu.Item
|
||||||
|
v-for="child in item.children"
|
||||||
|
:key="child.key"
|
||||||
|
@click="
|
||||||
|
child.path && route.path !== child.path && router.push(child.path)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Space size="small">
|
||||||
|
<component :is="child.icon" />
|
||||||
|
<span>{{ child.label }}</span>
|
||||||
|
</Space>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.SubMenu>
|
||||||
|
<Menu.Item
|
||||||
|
v-else
|
||||||
|
:key="`${item.key}-item`"
|
||||||
|
@click="item.path && route.path !== item.path && router.push(item.path)"
|
||||||
|
>
|
||||||
|
<Space size="small">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</Space>
|
||||||
|
</Menu.Item>
|
||||||
|
</template>
|
||||||
|
</Menu>
|
||||||
|
<Space>
|
||||||
|
<template v-if="user.isAuthenticated">
|
||||||
|
<Select
|
||||||
|
v-if="
|
||||||
|
user.userJoinedOrganizations && user.userJoinedOrganizations.length > 0
|
||||||
|
"
|
||||||
|
:value="user.userSelectedOrganization?.uuid ?? undefined"
|
||||||
|
@change="
|
||||||
|
(val) => {
|
||||||
|
const org = user.userJoinedOrganizations.find((o) => o.uuid === val)
|
||||||
|
user.setSelectedOrganization &&
|
||||||
|
user.setSelectedOrganization(org ?? null)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
style="min-width: 220px; margin-right: 0.5rem"
|
||||||
|
placeholder="Select organization"
|
||||||
|
>
|
||||||
|
<Select.Option
|
||||||
|
v-for="o in user.userJoinedOrganizations"
|
||||||
|
:key="o.uuid"
|
||||||
|
:value="o.uuid"
|
||||||
|
>
|
||||||
|
{{ o.name }}
|
||||||
|
</Select.Option>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Typography.Text class="user-chip" strong>
|
||||||
|
{{ user.displayName || 'Account' }}
|
||||||
|
</Typography.Text>
|
||||||
|
<Button ghost :loading="user.loading" @click="handleLogout">Logout</Button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<Button ghost @click="router.push('/login')">
|
||||||
|
<LoginOutlined />
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" @click="router.push('/register')">
|
||||||
|
<UserAddOutlined />
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Space>
|
||||||
|
</Layout.Header>
|
||||||
|
|
||||||
|
<Layout class="shell-body">
|
||||||
|
<Layout.Content class="shell-content">
|
||||||
|
<router-view />
|
||||||
|
</Layout.Content>
|
||||||
|
<Layout.Footer class="shell-footer">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
<strong>Project Disclaimer:</strong>
|
||||||
|
This is a proof-of-concept demo project for educational purposes. All
|
||||||
|
testimonials, statistics, and company names are fictional placeholders.
|
||||||
|
</Typography.Text>
|
||||||
|
</Layout.Footer>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0b1220;
|
||||||
|
}
|
||||||
|
.shell-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
.shell-menu {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.shell-body {
|
||||||
|
background: #0b1220;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.shell-content {
|
||||||
|
padding: 24px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: calc(100vh - 64px - 64px);
|
||||||
|
}
|
||||||
|
.shell-footer {
|
||||||
|
text-align: center;
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
:deep(.ant-menu-dark) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
:deep(.ant-menu-dark .ant-menu-item-selected) {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
:deep(.ant-typography),
|
||||||
|
:deep(.ant-typography p),
|
||||||
|
:deep(.ant-typography span),
|
||||||
|
:deep(.ant-list-item),
|
||||||
|
:deep(.ant-list-item-meta-title),
|
||||||
|
:deep(.ant-list-item-meta-description),
|
||||||
|
:deep(.ant-statistic-title),
|
||||||
|
:deep(.ant-statistic-content),
|
||||||
|
:deep(.ant-card-meta-title),
|
||||||
|
:deep(.ant-card-meta-description) {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
:deep(.ant-typography-secondary) {
|
||||||
|
color: #cbd5e1 !important;
|
||||||
|
}
|
||||||
|
:deep(.ant-form-item-label > label) {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
:deep(.ant-input),
|
||||||
|
:deep(.ant-select-selector),
|
||||||
|
:deep(.ant-select-selection-item),
|
||||||
|
:deep(.ant-picker-input input) {
|
||||||
|
background: #111827;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
:deep(.ant-input::placeholder),
|
||||||
|
:deep(.ant-select-selection-placeholder),
|
||||||
|
:deep(.ant-picker-input input::placeholder) {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
:deep(.ant-card) {
|
||||||
|
background: #0f172a;
|
||||||
|
border-color: #1f2937;
|
||||||
|
}
|
||||||
|
:deep(.ant-btn:not(.ant-btn-primary)) {
|
||||||
|
color: #e5e7eb;
|
||||||
|
border-color: #334155;
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
:deep(.ant-btn-primary) {
|
||||||
|
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.user-chip {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
:deep(.ant-typography-secondary) {
|
||||||
|
color: #cbd5e1 !important;
|
||||||
|
}
|
||||||
|
:deep(.ant-form-item-label > label) {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
:deep(.ant-input),
|
||||||
|
:deep(.ant-select-selector),
|
||||||
|
:deep(.ant-select-selection-item),
|
||||||
|
:deep(.ant-picker-input input) {
|
||||||
|
background: #111827;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border-color: #334155;
|
||||||
|
}
|
||||||
|
:deep(.ant-input::placeholder),
|
||||||
|
:deep(.ant-select-selection-placeholder),
|
||||||
|
:deep(.ant-picker-input input::placeholder) {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
:deep(.ant-card) {
|
||||||
|
background: #0f172a;
|
||||||
|
border-color: #1f2937;
|
||||||
|
}
|
||||||
|
:deep(.ant-btn:not(.ant-btn-primary)) {
|
||||||
|
color: #e5e7eb;
|
||||||
|
border-color: #334155;
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
:deep(.ant-btn-primary) {
|
||||||
|
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.user-chip {
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
site/src/css/styles.css
Normal file
95
site/src/css/styles.css
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
font-family:
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
'Helvetica Neue',
|
||||||
|
Arial,
|
||||||
|
'Noto Sans',
|
||||||
|
sans-serif,
|
||||||
|
'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji',
|
||||||
|
'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji';
|
||||||
|
line-height: 1.5;
|
||||||
|
tab-size: 4;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
background: #0b1220;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p,
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: currentColor;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--view-max-width: 1200px;
|
||||||
|
--view-panel-background: #0f172a;
|
||||||
|
--view-panel-border: #1f2937;
|
||||||
|
--view-panel-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--view-max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: var(--view-panel-background);
|
||||||
|
border: 1px solid var(--view-panel-border);
|
||||||
|
color: var(--view-panel-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .ant-card-body {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .ant-typography,
|
||||||
|
.panel .ant-typography *,
|
||||||
|
.panel .ant-list-item-meta-title,
|
||||||
|
.panel .ant-list-item-meta-description,
|
||||||
|
.panel .ant-statistic-title,
|
||||||
|
.panel .ant-statistic-content {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .ant-typography-secondary,
|
||||||
|
.panel .ant-typography-secondary * {
|
||||||
|
color: rgba(255, 255, 255, 0.7) !important;
|
||||||
|
}
|
||||||
15
site/src/main.ts
Normal file
15
site/src/main.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import './css/styles.css'
|
||||||
|
import 'ant-design-vue/dist/reset.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
128
site/src/router/api.ts
Normal file
128
site/src/router/api.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
|
||||||
|
class ApiClient {
|
||||||
|
private client: AxiosInstance
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.client = axios.create({
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCsrfToken(): string {
|
||||||
|
let cookieValue = ''
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';')
|
||||||
|
for (const rawCookie of cookies) {
|
||||||
|
const cookie = (rawCookie || '').trim()
|
||||||
|
if (cookie.startsWith('csrftoken=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.slice('csrftoken='.length))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue
|
||||||
|
}
|
||||||
|
|
||||||
|
private withCsrf(config?: AxiosRequestConfig): AxiosRequestConfig {
|
||||||
|
const token = this.getCsrfToken()
|
||||||
|
const csrfHeader = token ? { 'X-CSRFToken': token } : {}
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
headers: {
|
||||||
|
...csrfHeader,
|
||||||
|
...(config?.headers || {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||||
|
return this.client.get<T>(url, this.withCsrf(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
post<T = unknown>(
|
||||||
|
url: string,
|
||||||
|
data?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<AxiosResponse<T>> {
|
||||||
|
return this.client.post<T>(url, data, this.withCsrf(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
put<T = unknown>(
|
||||||
|
url: string,
|
||||||
|
data?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<AxiosResponse<T>> {
|
||||||
|
return this.client.put<T>(url, data, this.withCsrf(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
patch<T = unknown>(
|
||||||
|
url: string,
|
||||||
|
data?: unknown,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<AxiosResponse<T>> {
|
||||||
|
return this.client.patch<T>(url, data, this.withCsrf(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
|
||||||
|
return this.client.delete<T>(url, this.withCsrf(config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const API = {
|
||||||
|
me: () => '/api/user/me/',
|
||||||
|
login: () => '/api/user/login/',
|
||||||
|
logout: () => '/api/user/logout/',
|
||||||
|
session: () => '/api/user/session/',
|
||||||
|
signup: () => '/api/user/signup/',
|
||||||
|
changePassword: () => '/api/user/change_password/',
|
||||||
|
|
||||||
|
organizations: () => '/api/organization/',
|
||||||
|
organization: (uuid: string) => `/api/organization/${uuid}/`,
|
||||||
|
organizationMembers: (uuid: string) => `/api/organization/${uuid}/members/`,
|
||||||
|
organizationMemberRemove: (uuid: string, userId: number) =>
|
||||||
|
`/api/organization/${uuid}/member/${userId}/remove/`,
|
||||||
|
|
||||||
|
organizationInvites: (uuid: string) => `/api/organization/${uuid}/invite/`,
|
||||||
|
organizationCreateInvite: (uuid: string, max_uses: number) =>
|
||||||
|
`/api/organization/${uuid}/create-invite/?max_uses=${max_uses}`,
|
||||||
|
organizationRevokeInvite: (uuid: string, inviteUuid: string) =>
|
||||||
|
`/api/organization/${uuid}/revoke-invite/${inviteUuid}/`,
|
||||||
|
organizationJoin: (token: string) => `/api/organization/join/${token}/`,
|
||||||
|
organizationLeave: (uuid: string) => `/api/organization/${uuid}/leave/`,
|
||||||
|
|
||||||
|
organizationRoles: (uuid: string) => `/api/organization/${uuid}/role/`,
|
||||||
|
organizationRolesMine: () => '/api/organization/role/mine/',
|
||||||
|
organizationRoleDelete: (orgUuid: string, roleUuid: string) =>
|
||||||
|
`/api/organization/${orgUuid}/role/${roleUuid}/`,
|
||||||
|
organizationRoleJoin: (orgUuid: string, roleUuid: string) =>
|
||||||
|
`/api/organization/${orgUuid}/role/${roleUuid}/join/`,
|
||||||
|
|
||||||
|
trainingFiles: () => `/api/training-file/`,
|
||||||
|
trainingFile: (uuid: string) => `/api/training-file/${uuid}/`,
|
||||||
|
organizationTrainingFiles: () => `/api/training-file/`,
|
||||||
|
organizationTrainingFile: (uuid: string) => `/api/training-file/${uuid}/`,
|
||||||
|
roleRagDocuments: () => `/api/role-rag-document/`,
|
||||||
|
roleRagDocument: (uuid: string) => `/api/role-rag-document/${uuid}/`,
|
||||||
|
|
||||||
|
agentConfigs: () => '/api/agent-config/',
|
||||||
|
agentConfig: (uuid: string) => `/api/agent-config/${uuid}/`,
|
||||||
|
|
||||||
|
onboardingFlows: () => '/api/onboarding-flow/',
|
||||||
|
onboardingFlow: (uuid: string) => `/api/onboarding-flow/${uuid}/`,
|
||||||
|
onboardingFlowStartSession: (uuid: string) => `/api/onboarding-flow/${uuid}/start-session/`,
|
||||||
|
|
||||||
|
onboardingSessions: () => '/api/onboarding-session/',
|
||||||
|
onboardingSession: (uuid: string) => `/api/onboarding-session/${uuid}/`,
|
||||||
|
onboardingSessionInteract: (uuid: string) => `/api/onboarding-session/${uuid}/interact/`,
|
||||||
|
onboardingSessionHistory: (uuid: string) => `/api/onboarding-session/${uuid}/history/`,
|
||||||
|
onboardingSessionComplete: (uuid: string) => `/api/onboarding-session/${uuid}/complete/`,
|
||||||
|
|
||||||
|
interactionLogs: () => '/api/agent-interaction-log/',
|
||||||
|
interactionLog: (uuid: string) => `/api/agent-interaction-log/${uuid}/`,
|
||||||
|
}
|
||||||
|
export const apiClient = new ApiClient()
|
||||||
|
export { isAxiosError } from 'axios'
|
||||||
109
site/src/router/index.ts
Normal file
109
site/src/router/index.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { useUserStore } from '../stores/userStore'
|
||||||
|
|
||||||
|
const BASE_URL = (import.meta as unknown as { env: { BASE_URL?: string } }).env?.BASE_URL || '/'
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: () => import('../views/HomeView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/about',
|
||||||
|
name: 'about',
|
||||||
|
component: () => import('../views/AboutView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/pricing',
|
||||||
|
name: 'pricing',
|
||||||
|
component: () => import('../views/PricingView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('../views/LoginView.vue'),
|
||||||
|
meta: { guestOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'register',
|
||||||
|
component: () => import('../views/RegisterView.vue'),
|
||||||
|
meta: { guestOnly: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/organization',
|
||||||
|
name: 'organization',
|
||||||
|
component: () => import('../views/OrganizationsView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/organization/:id',
|
||||||
|
name: 'organization-detail',
|
||||||
|
component: () => import('../views/OrganizationView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/organization/:id/manage',
|
||||||
|
name: 'organization-manage',
|
||||||
|
component: () => import('../views/OrganizationManage.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresManager: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/invite/:token',
|
||||||
|
name: 'invite-accept',
|
||||||
|
component: () => import('../views/InviteAccept.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/agents',
|
||||||
|
name: 'agents',
|
||||||
|
component: () => import('../views/AgentsView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresManager: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/agents/:id',
|
||||||
|
name: 'agent-detail',
|
||||||
|
component: () => import('../views/AgentDetailView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresManager: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/onboarding/:roleId',
|
||||||
|
name: 'onboarding-role',
|
||||||
|
component: () => import('../views/OnboardingView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/progress',
|
||||||
|
name: 'progress',
|
||||||
|
component: () => import('../views/ProgressView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/progress/:roleId',
|
||||||
|
name: 'progress-role',
|
||||||
|
component: () => import('../views/ProgressDetailView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const isAuthenticated = userStore.isAuthenticated
|
||||||
|
const isManager = userStore.isGeneralManager
|
||||||
|
|
||||||
|
if (to.meta?.guestOnly && isAuthenticated) {
|
||||||
|
return next({ path: '/' })
|
||||||
|
}
|
||||||
|
if (to.meta?.requiresAuth && !isAuthenticated) {
|
||||||
|
return next({ path: '/login', query: { redirect: to.fullPath } })
|
||||||
|
}
|
||||||
|
if (to.meta?.requiresManager && !isManager) {
|
||||||
|
return next({ path: '/' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
139
site/src/stores/agentStore.ts
Normal file
139
site/src/stores/agentStore.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import type { AgentEvent } from '../types/agent'
|
||||||
|
|
||||||
|
export const useAgentStore = defineStore('agent', () => {
|
||||||
|
const isConnected = ref(false)
|
||||||
|
const executionStatus = ref<'idle' | 'running' | 'completed' | 'failed'>('idle')
|
||||||
|
const eventLog = ref<AgentEvent[]>([])
|
||||||
|
const lastExecutionId = ref<string | null>(null)
|
||||||
|
const socket = ref<WebSocket | null>(null)
|
||||||
|
|
||||||
|
const pushEvent = (evt: {
|
||||||
|
type: string
|
||||||
|
message?: string
|
||||||
|
content?: unknown
|
||||||
|
timestamp?: string
|
||||||
|
}) => {
|
||||||
|
eventLog.value.unshift({
|
||||||
|
type: evt.type,
|
||||||
|
message: evt.message,
|
||||||
|
content: evt.content,
|
||||||
|
timestamp: evt.timestamp ? new Date(evt.timestamp) : new Date(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const connect = (id: string) => {
|
||||||
|
if (socket.value) {
|
||||||
|
socket.value.close()
|
||||||
|
socket.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
|
const wsUrl = `${wsProtocol}://${window.location.host}/ws/onboarding/${id}/`
|
||||||
|
socket.value = new WebSocket(wsUrl)
|
||||||
|
|
||||||
|
socket.value.onopen = () => {
|
||||||
|
isConnected.value = true
|
||||||
|
pushEvent({ type: 'status', message: 'Connected to Orchestrator' })
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.value.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(event.data)
|
||||||
|
const type = payload.type
|
||||||
|
|
||||||
|
if (payload.execution_id) {
|
||||||
|
lastExecutionId.value = String(payload.execution_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'status' || type === 'thought' || type === 'tool_start') {
|
||||||
|
executionStatus.value = 'running'
|
||||||
|
pushEvent({
|
||||||
|
type,
|
||||||
|
message: payload.message || payload.thought,
|
||||||
|
content: payload.content,
|
||||||
|
})
|
||||||
|
} else if (
|
||||||
|
type === 'tool_call' ||
|
||||||
|
type === 'tool_result' ||
|
||||||
|
type === 'tool_complete'
|
||||||
|
) {
|
||||||
|
pushEvent({
|
||||||
|
type,
|
||||||
|
message: payload.message,
|
||||||
|
content: payload.content || payload,
|
||||||
|
})
|
||||||
|
} else if (type === 'completed') {
|
||||||
|
executionStatus.value = 'completed'
|
||||||
|
pushEvent({
|
||||||
|
type: 'completed',
|
||||||
|
message: 'Generation loop finished successfully',
|
||||||
|
content: payload.content,
|
||||||
|
timestamp: payload.timestamp,
|
||||||
|
})
|
||||||
|
} else if (type === 'error') {
|
||||||
|
executionStatus.value = 'failed'
|
||||||
|
pushEvent({ type: 'error', message: payload.message })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Store message error', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.value.onclose = () => {
|
||||||
|
isConnected.value = false
|
||||||
|
executionStatus.value = 'idle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const disconnect = () => {
|
||||||
|
if (socket.value) {
|
||||||
|
socket.value.close()
|
||||||
|
socket.value = null
|
||||||
|
}
|
||||||
|
isConnected.value = false
|
||||||
|
executionStatus.value = 'idle'
|
||||||
|
}
|
||||||
|
|
||||||
|
const startAgent = (data: { query?: string; role_uuid?: string; max_tokens?: number }) => {
|
||||||
|
if (!socket.value || socket.value.readyState !== WebSocket.OPEN) return
|
||||||
|
|
||||||
|
executionStatus.value = 'running'
|
||||||
|
socket.value.send(
|
||||||
|
JSON.stringify({
|
||||||
|
query: data.query,
|
||||||
|
role_uuid: data.role_uuid,
|
||||||
|
max_tokens: data.max_tokens,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAgent = (executionId?: string) => {
|
||||||
|
if (!socket.value || socket.value.readyState !== WebSocket.OPEN) return
|
||||||
|
|
||||||
|
socket.value.send(
|
||||||
|
JSON.stringify({
|
||||||
|
action: 'stop_agent',
|
||||||
|
execution_id: executionId ?? lastExecutionId.value,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearLog = () => {
|
||||||
|
eventLog.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected,
|
||||||
|
executionStatus,
|
||||||
|
eventLog,
|
||||||
|
socket,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
startAgent,
|
||||||
|
stopAgent,
|
||||||
|
clearLog,
|
||||||
|
lastExecutionId,
|
||||||
|
}
|
||||||
|
})
|
||||||
218
site/src/stores/userStore.ts
Normal file
218
site/src/stores/userStore.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { apiClient, isAxiosError, API } from '../router/api'
|
||||||
|
import type { User, SessionResponse } from '../types/user'
|
||||||
|
import type { Organization, Role } from 'src/types/organization'
|
||||||
|
|
||||||
|
const orgUuidKey = 'userSelectedOrganizationUuid'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const isAuthenticated = ref(false)
|
||||||
|
const isAdmin = ref(false)
|
||||||
|
const isGeneralManager = computed(() => {
|
||||||
|
if (!isAuthenticated.value || !user.value) return false
|
||||||
|
return user.value.is_manager
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
const user = ref<User | null>(null)
|
||||||
|
const userJoinedOrganizations = ref<Organization[]>([])
|
||||||
|
const userSelectedOrganization = ref<Organization | null>(null)
|
||||||
|
const userJoinedRoles = ref<Role[]>([])
|
||||||
|
|
||||||
|
const displayName = computed(() => {
|
||||||
|
if (!user.value) return ''
|
||||||
|
return `${user.value.first_name} ${user.value.last_name}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const setUser = (userData: User | null) => {
|
||||||
|
user.value = userData
|
||||||
|
isAuthenticated.value = !!userData
|
||||||
|
}
|
||||||
|
|
||||||
|
const setJoinedOrganizations = (organizations: Organization[]) => {
|
||||||
|
userJoinedOrganizations.value = organizations
|
||||||
|
if (organizations.length > 0) {
|
||||||
|
const stored = localStorage.getItem(orgUuidKey)
|
||||||
|
const matched = organizations.find((org) => org.uuid === stored)
|
||||||
|
if (matched) {
|
||||||
|
userSelectedOrganization.value = matched
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userSelectedOrganization.value = organizations[0]
|
||||||
|
localStorage.setItem(orgUuidKey, organizations[0].uuid)
|
||||||
|
} else {
|
||||||
|
userSelectedOrganization.value = null
|
||||||
|
localStorage.removeItem(orgUuidKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSelectedOrganization = (organization: Organization | null) => {
|
||||||
|
userSelectedOrganization.value = organization
|
||||||
|
if (organization) {
|
||||||
|
localStorage.setItem(orgUuidKey, organization.uuid)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(orgUuidKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSession = async (force = false) => {
|
||||||
|
if (isAuthenticated.value && !force) return user.value
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<SessionResponse>(API.session())
|
||||||
|
if (response.data?.isAuthenticated) {
|
||||||
|
const userData = await apiClient.get<User>(API.me())
|
||||||
|
setUser(userData.data)
|
||||||
|
await fetchJoinedOrganizations()
|
||||||
|
} else {
|
||||||
|
setUser(null)
|
||||||
|
isAuthenticated.value = false
|
||||||
|
}
|
||||||
|
return user.value
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (isAxiosError(err)) {
|
||||||
|
error.value = err.response?.data?.detail || err.response?.data?.error || err.message
|
||||||
|
} else if (err instanceof Error) {
|
||||||
|
error.value = err.message
|
||||||
|
} else {
|
||||||
|
error.value = String(err)
|
||||||
|
}
|
||||||
|
setUser(null)
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchJoinedOrganizations = async () => {
|
||||||
|
if (!user.value) return
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<Organization[]>(API.organizations())
|
||||||
|
setJoinedOrganizations(response.data)
|
||||||
|
return response.data
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error('Failed to fetch organizations', err)
|
||||||
|
setJoinedOrganizations([])
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchJoinedRoles = async () => {
|
||||||
|
if (!user.value || !userSelectedOrganization.value) return
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<Role[]>(
|
||||||
|
API.organizationRoles(userSelectedOrganization.value.uuid),
|
||||||
|
)
|
||||||
|
setJoinedRoles(response.data)
|
||||||
|
return response.data
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error('Failed to fetch role memberships', err)
|
||||||
|
setJoinedRoles([])
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setJoinedRoles = (roles: Role[]) => {
|
||||||
|
userJoinedRoles.value = roles
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async (emailAddress: string, password: string) => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const res = await apiClient.post<{
|
||||||
|
user: User
|
||||||
|
message?: string
|
||||||
|
}>(API.login(), { email_address: emailAddress, password })
|
||||||
|
setUser(res.data?.user ?? null)
|
||||||
|
return res.data
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (isAxiosError(err)) {
|
||||||
|
error.value = err.response?.data?.error || err.response?.data?.detail || err.message
|
||||||
|
} else if (err instanceof Error) {
|
||||||
|
error.value = err.message
|
||||||
|
} else {
|
||||||
|
error.value = String(err)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = async (payload: {
|
||||||
|
email_address: string
|
||||||
|
password: string
|
||||||
|
confirm_password?: string
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
date_of_birth?: string
|
||||||
|
manager: boolean
|
||||||
|
}) => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
await apiClient.post(API.signup(), payload)
|
||||||
|
await login(payload.email_address, payload.password)
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (isAxiosError(err)) {
|
||||||
|
error.value = err.response?.data?.detail || err.response?.data?.error || err.message
|
||||||
|
} else if (err instanceof Error) {
|
||||||
|
error.value = err.message
|
||||||
|
} else {
|
||||||
|
error.value = String(err)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
await apiClient.post(API.logout())
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (isAxiosError(err)) {
|
||||||
|
error.value = err.response?.data?.detail || err.response?.data?.error || err.message
|
||||||
|
} else if (err instanceof Error) {
|
||||||
|
error.value = err.message
|
||||||
|
} else {
|
||||||
|
error.value = String(err)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
setUser(null)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
displayName,
|
||||||
|
isAuthenticated,
|
||||||
|
isAdmin,
|
||||||
|
isGeneralManager,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
userJoinedOrganizations,
|
||||||
|
userSelectedOrganization,
|
||||||
|
userJoinedRoles,
|
||||||
|
|
||||||
|
setUser,
|
||||||
|
fetchSession,
|
||||||
|
setJoinedOrganizations,
|
||||||
|
setSelectedOrganization,
|
||||||
|
setJoinedRoles,
|
||||||
|
fetchJoinedOrganizations,
|
||||||
|
fetchJoinedRoles,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
}
|
||||||
|
})
|
||||||
6
site/src/types/agent.ts
Normal file
6
site/src/types/agent.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export type AgentEvent = {
|
||||||
|
type: string
|
||||||
|
timestamp: Date
|
||||||
|
message?: string
|
||||||
|
content?: unknown
|
||||||
|
}
|
||||||
44
site/src/types/onboarding.ts
Normal file
44
site/src/types/onboarding.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
export type OnboardingField = {
|
||||||
|
uuid: string
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
field_type: 'text' | 'textarea' | 'select' | 'multiselect' | 'number' | 'boolean' | 'date'
|
||||||
|
required: boolean
|
||||||
|
help_text?: string
|
||||||
|
placeholder?: string
|
||||||
|
options?: string[]
|
||||||
|
default_value?: unknown
|
||||||
|
validation?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnboardingPage = {
|
||||||
|
uuid: string
|
||||||
|
order: number
|
||||||
|
title: string
|
||||||
|
body?: string
|
||||||
|
meta?: Record<string, unknown>
|
||||||
|
fields: OnboardingField[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnboardingFlow = {
|
||||||
|
uuid: string
|
||||||
|
role: string
|
||||||
|
agent?: string | null
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
status: 'draft' | 'published' | 'archived'
|
||||||
|
pages?: OnboardingPage[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnboardingSession = {
|
||||||
|
uuid: string
|
||||||
|
flow: string
|
||||||
|
agent_run?: string | null
|
||||||
|
status: 'in_progress' | 'completed' | 'abandoned'
|
||||||
|
current_page_order: number
|
||||||
|
responses: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnboardingFeedback = {
|
||||||
|
summary?: string
|
||||||
|
}
|
||||||
53
site/src/types/organization.ts
Normal file
53
site/src/types/organization.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { User } from './user'
|
||||||
|
|
||||||
|
export interface Organization {
|
||||||
|
id: number
|
||||||
|
uuid: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
owner: User
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
member_count?: number
|
||||||
|
role_count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Role {
|
||||||
|
id: number
|
||||||
|
uuid: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
organization: Organization
|
||||||
|
member_count?: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InviteToken {
|
||||||
|
id: number
|
||||||
|
token: string
|
||||||
|
invite_url: string
|
||||||
|
created_by: User
|
||||||
|
organization: Organization
|
||||||
|
is_active: boolean
|
||||||
|
expires_at: string
|
||||||
|
is_valid: boolean
|
||||||
|
max_uses?: number
|
||||||
|
uses?: number
|
||||||
|
}
|
||||||
|
export interface TrainingFile {
|
||||||
|
id: number
|
||||||
|
uuid: string
|
||||||
|
role: Role
|
||||||
|
uploaded_by: User
|
||||||
|
file: string
|
||||||
|
file_name: string
|
||||||
|
file_size: number
|
||||||
|
file_type: string
|
||||||
|
description: string
|
||||||
|
is_processed: boolean
|
||||||
|
status: 'ingesting' | 'chunked' | 'embedded' | 'failed'
|
||||||
|
file_url: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
18
site/src/types/user.ts
Normal file
18
site/src/types/user.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
export interface User {
|
||||||
|
id: number
|
||||||
|
uuid: string
|
||||||
|
email_address: string
|
||||||
|
first_name: string
|
||||||
|
last_name: string
|
||||||
|
date_of_birth?: string
|
||||||
|
timezone?: string
|
||||||
|
avatar_url?: string
|
||||||
|
is_manager: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionResponse {
|
||||||
|
isAuthenticated: boolean
|
||||||
|
isStaff: boolean
|
||||||
|
}
|
||||||
110
site/src/views/AboutView.vue
Normal file
110
site/src/views/AboutView.vue
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Card, Typography, Divider, List } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
'Register or login.',
|
||||||
|
'Complete onboarding and training to simulate a role journey.',
|
||||||
|
'Managers review onboarding completion and feedback.',
|
||||||
|
]
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
title: 'Modular Content',
|
||||||
|
desc: 'Compose learning journeys from small, reusable modules. Mix videos and interactive checks.',
|
||||||
|
img: 'https://placehold.co/600x400?text=Modular+Content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Agent Workflows',
|
||||||
|
desc: 'Automate guidance and triggers with configurable agents to move users through onboarding steps.',
|
||||||
|
img: 'https://placehold.co/600x400?text=Agent+Workflows',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Reporting & Insights',
|
||||||
|
desc: 'Lightweight reports showing completion and engagement metrics.',
|
||||||
|
img: 'https://placehold.co/600x400?text=Reporting',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Card class="panel" :bordered="false">
|
||||||
|
<Typography.Title :level="2">About Dynavera</Typography.Title>
|
||||||
|
<Typography.Paragraph type="secondary">
|
||||||
|
Dynavera is a lightweight platform for onboarding, training, and assessing employees
|
||||||
|
with modular content and agent-driven workflows. It is designed for teams that want
|
||||||
|
tangible learning experiences quickly without complex LMS setup.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Divider />
|
||||||
|
<Typography.Title :level="4">Getting started</Typography.Title>
|
||||||
|
<List :data-source="steps" :bordered="false">
|
||||||
|
<template #renderItem="{ item, index }">
|
||||||
|
<List.Item class="row">
|
||||||
|
<strong>{{ index + 1 }}.</strong>
|
||||||
|
{{ item }}
|
||||||
|
</List.Item>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<Typography.Title :level="4">Features</Typography.Title>
|
||||||
|
<div class="features">
|
||||||
|
<div v-for="f in features" :key="f.title" class="feature-card">
|
||||||
|
<img :src="f.img" :alt="f.title" class="feature-hero" />
|
||||||
|
<div class="feature-body">
|
||||||
|
<Typography.Text strong>{{ f.title }}</Typography.Text>
|
||||||
|
<Typography.Paragraph type="secondary">{{ f.desc }}</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<Typography.Title :level="4">More about Dynavera</Typography.Title>
|
||||||
|
<Typography.Paragraph>
|
||||||
|
Dynavera is built to be extensible. Plug your content sources, connect an LMS, or
|
||||||
|
integrate third-party learning tools. The platform focuses on flexibility and ease
|
||||||
|
of use, so teams can get started quickly and adapt as their needs evolve. Whether
|
||||||
|
you are a small startup or a growing enterprise, Dynavera aims to simplify the
|
||||||
|
process of onboarding and training with modern, agent-driven approaches.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
max-width: 1100px;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
width: 100%;
|
||||||
|
height: 320px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.features {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.feature-card {
|
||||||
|
background: var(--ant-card-background, #08121a);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.feature-hero {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.feature-body {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
362
site/src/views/AgentDetailView.vue
Normal file
362
site/src/views/AgentDetailView.vue
Normal file
|
|
@ -0,0 +1,362 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
Space,
|
||||||
|
Spin,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Tag,
|
||||||
|
InputNumber,
|
||||||
|
} from 'ant-design-vue'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
import { useAgentStore } from '../stores/agentStore'
|
||||||
|
import { apiClient, isAxiosError, API } from '../router/api'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const agentStore = useAgentStore()
|
||||||
|
|
||||||
|
const agentId = route.params.id as string
|
||||||
|
|
||||||
|
const agent = ref<Record<string, unknown>>({
|
||||||
|
id: agentId,
|
||||||
|
name: 'Loading...',
|
||||||
|
description: '',
|
||||||
|
status: 'idle',
|
||||||
|
})
|
||||||
|
const maxTokens = ref<number>(256)
|
||||||
|
|
||||||
|
const queryInput = ref('')
|
||||||
|
const isRunning = computed(() => agentStore.executionStatus === 'running')
|
||||||
|
const isConnected = computed(() => agentStore.isConnected ?? false)
|
||||||
|
|
||||||
|
const agentResponse = computed(() => {
|
||||||
|
const completedEvent = agentStore.eventLog?.find((event) => event.type === 'completed')
|
||||||
|
if (completedEvent?.content && typeof completedEvent.content === 'object') {
|
||||||
|
const output = completedEvent.content as Record<string, unknown>
|
||||||
|
|
||||||
|
return (output.response as string) || null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusColor = (status: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
idle: 'default',
|
||||||
|
running: 'processing',
|
||||||
|
completed: 'success',
|
||||||
|
failed: 'error',
|
||||||
|
stopped: 'warning',
|
||||||
|
}
|
||||||
|
return colors[status] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAgent = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<Record<string, unknown>>(API.agentConfig(agentId))
|
||||||
|
agent.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch agent:', error)
|
||||||
|
if (isAxiosError(error)) {
|
||||||
|
console.error('Axios error details:', {
|
||||||
|
status: error.response?.status,
|
||||||
|
data: error.response?.data,
|
||||||
|
message: error.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
message.error('Failed to load agent details')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedAgentResponse = computed(() => {
|
||||||
|
const rawMarkdown = agentResponse.value
|
||||||
|
if (!rawMarkdown) return ''
|
||||||
|
|
||||||
|
const html = marked.parse(rawMarkdown) as string
|
||||||
|
return DOMPurify.sanitize(html)
|
||||||
|
})
|
||||||
|
|
||||||
|
const startAgent = () => {
|
||||||
|
if (!agentStore.isConnected) {
|
||||||
|
message.error('WebSocket not connected')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!queryInput.value.trim()) {
|
||||||
|
message.error('Please enter a query')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agentStore.startAgent({
|
||||||
|
query: queryInput.value.trim(),
|
||||||
|
max_tokens: maxTokens.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAgent = () => {
|
||||||
|
agentStore.stopAgent(agentStore.lastExecutionId || undefined)
|
||||||
|
message.success('Agent stop requested')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchAgent()
|
||||||
|
agentStore.connect(agentId)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
agentStore.disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Card class="panel" :bordered="false">
|
||||||
|
<div class="header">
|
||||||
|
<Typography.Title :level="2">{{ agent.name }}</Typography.Title>
|
||||||
|
<Tag :color="statusColor(String(agentStore.executionStatus || 'idle'))">
|
||||||
|
{{ (agentStore.executionStatus || 'idle').toString().toUpperCase() }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography.Paragraph type="secondary">
|
||||||
|
{{ agent.description || 'No description available' }}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
|
||||||
|
<div class="connection-status">
|
||||||
|
<span>WebSocket Status:</span>
|
||||||
|
<Tag :color="agentStore.isConnected ? 'green' : 'red'">
|
||||||
|
{{ agentStore.isConnected ? 'CONNECTED' : 'DISCONNECTED' }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography.Title :level="4" class="section-title">Execution</Typography.Title>
|
||||||
|
|
||||||
|
<div class="execution-controls">
|
||||||
|
<Space direction="vertical" style="width: 100%">
|
||||||
|
<div>
|
||||||
|
<Typography.Text>Query:</Typography.Text>
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="queryInput"
|
||||||
|
:disabled="isRunning"
|
||||||
|
placeholder="Enter your query here..."
|
||||||
|
:rows="4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Typography.Text>Max Tokens:</Typography.Text>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="maxTokens"
|
||||||
|
:min="1"
|
||||||
|
:max="4096"
|
||||||
|
:disabled="isRunning"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:disabled="isRunning || !isConnected"
|
||||||
|
@click="startAgent"
|
||||||
|
>
|
||||||
|
Run Agent
|
||||||
|
</Button>
|
||||||
|
<Button danger :disabled="!isRunning" @click="stopAgent">Stop Agent</Button>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="agentResponse" class="response-section">
|
||||||
|
<Typography.Title :level="4" class="section-title">Final Response</Typography.Title>
|
||||||
|
<Card class="response-card response-final" :bordered="false">
|
||||||
|
<div
|
||||||
|
class="response-content markdown-body"
|
||||||
|
v-html="renderedAgentResponse"
|
||||||
|
></div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<Typography.Title :level="4" class="section-title">Execution Log</Typography.Title>
|
||||||
|
|
||||||
|
<Spin :spinning="isRunning" tip="Agent running...">
|
||||||
|
<div class="log-container">
|
||||||
|
<List
|
||||||
|
v-if="(agentStore.eventLog?.length ?? 0) > 0"
|
||||||
|
:data-source="agentStore.eventLog || []"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #renderItem="{ item }">
|
||||||
|
<List.Item class="log-item">
|
||||||
|
<div class="log-entry">
|
||||||
|
<Tag class="log-type">{{ item.type }}</Tag>
|
||||||
|
<span class="log-time">
|
||||||
|
{{ item.timestamp.toLocaleTimeString() }}
|
||||||
|
</span>
|
||||||
|
<div v-if="item.message" class="log-message">
|
||||||
|
{{ item.message }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="item.content && typeof item.content === 'object'"
|
||||||
|
class="log-content"
|
||||||
|
>
|
||||||
|
<pre>{{ JSON.stringify(item.content, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.content" class="log-content">
|
||||||
|
{{ item.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</List.Item>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
<Typography.Paragraph v-else type="secondary">
|
||||||
|
No events yet. Start the agent to see execution logs.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-top: 2rem !important;
|
||||||
|
margin-bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #1f2937;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.execution-controls {
|
||||||
|
background: #1f2937;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container {
|
||||||
|
background: #1f2937;
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
border-bottom: 1px solid #374151 !important;
|
||||||
|
padding: 0.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-type {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
background: #111827;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content pre {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-section {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-card {
|
||||||
|
background: #1f2937;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-final {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-content {
|
||||||
|
color: #e5e7eb;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(h1),
|
||||||
|
.markdown-body :deep(h2),
|
||||||
|
.markdown-body :deep(h3) {
|
||||||
|
color: #f8fafc;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(ul),
|
||||||
|
.markdown-body :deep(ol) {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(code) {
|
||||||
|
background: #020617;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body :deep(p) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
132
site/src/views/AgentsView.vue
Normal file
132
site/src/views/AgentsView.vue
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { List, Typography, Button, Card, Spin, message, Tag, Space } from 'ant-design-vue'
|
||||||
|
import { apiClient, API } from '../router/api'
|
||||||
|
|
||||||
|
interface LLMConfig {
|
||||||
|
model_id?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OrganizationRef {
|
||||||
|
uuid?: string
|
||||||
|
name?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Agent {
|
||||||
|
uuid: string
|
||||||
|
name: string
|
||||||
|
agent_type: string
|
||||||
|
llm_config: LLMConfig
|
||||||
|
organization: string | OrganizationRef
|
||||||
|
}
|
||||||
|
|
||||||
|
const agents = ref<Agent[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadError = ref(false)
|
||||||
|
|
||||||
|
type MaybePaginated<T> = T[] | { results?: T[] }
|
||||||
|
|
||||||
|
const fetchAgents = async () => {
|
||||||
|
loading.value = true
|
||||||
|
loadError.value = false
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<MaybePaginated<Agent>>(API.agentConfigs())
|
||||||
|
|
||||||
|
const data = response.data
|
||||||
|
agents.value = Array.isArray(data) ? data : data.results || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch agents:', error)
|
||||||
|
message.error('Failed to load agent configurations')
|
||||||
|
loadError.value = true
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAgentTypeLabel = (type: string) => {
|
||||||
|
const types: Record<string, string> = {
|
||||||
|
curriculum: 'Curriculum Agent',
|
||||||
|
knowledge: 'Knowledge Agent',
|
||||||
|
assessment: 'Assessment Agent',
|
||||||
|
monitor: 'Progress Monitor',
|
||||||
|
}
|
||||||
|
return types[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchAgents()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Typography.Title :level="2">Agent Configurations</Typography.Title>
|
||||||
|
<Typography.Paragraph type="secondary">
|
||||||
|
Manage your AI personas and their specific tool permissions.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
|
||||||
|
<Card class="panel" :bordered="false">
|
||||||
|
<Spin :spinning="loading" tip="Loading Agents...">
|
||||||
|
<div v-if="loadError" class="empty">
|
||||||
|
<Typography.Paragraph type="danger">
|
||||||
|
Failed to load agents.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!loading && agents.length === 0" class="empty">
|
||||||
|
<Typography.Paragraph type="secondary">
|
||||||
|
No agent configurations found.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<List v-else :data-source="agents" item-layout="horizontal">
|
||||||
|
<template #renderItem="{ item }">
|
||||||
|
<List.Item class="item">
|
||||||
|
<List.Item.Meta :title="item.name">
|
||||||
|
<template #description>
|
||||||
|
<Space direction="vertical">
|
||||||
|
<Tag color="blue">
|
||||||
|
{{ getAgentTypeLabel(item.agent_type) }}
|
||||||
|
</Tag>
|
||||||
|
<span class="config-summary">
|
||||||
|
Model: {{ item.llm_config?.model_id || 'Default' }}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
</List.Item.Meta>
|
||||||
|
<RouterLink :to="`/agents/${item.uuid}`">
|
||||||
|
<Button type="primary">Manage & Run</Button>
|
||||||
|
</RouterLink>
|
||||||
|
</List.Item>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
</Spin>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
}
|
||||||
|
.item :deep(.ant-list-item-meta-title) {
|
||||||
|
color: #f8fafc;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.config-summary {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.empty {
|
||||||
|
padding: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
273
site/src/views/HomeView.vue
Normal file
273
site/src/views/HomeView.vue
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Tag,
|
||||||
|
Statistic,
|
||||||
|
Carousel,
|
||||||
|
Avatar,
|
||||||
|
Space,
|
||||||
|
Divider,
|
||||||
|
} from 'ant-design-vue'
|
||||||
|
import { CheckCircleTwoTone, ThunderboltTwoTone, CloudTwoTone } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const heroImage =
|
||||||
|
'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1400&q=80'
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ title: 'Teams Onboarded', value: '240+' },
|
||||||
|
{ title: 'Avg. Time Saved', value: '38%' },
|
||||||
|
{ title: 'Playbooks Ready', value: '120' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
title: 'Adaptive AI Guides',
|
||||||
|
description:
|
||||||
|
'Role-specific checklists, interactive tours, and contextual help tuned to your stack.',
|
||||||
|
icon: CheckCircleTwoTone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Skills & Assessments',
|
||||||
|
description:
|
||||||
|
'Scenario-based quizzes and code tasks with instant insights and coach-like feedback.',
|
||||||
|
icon: ThunderboltTwoTone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Knowledge Mesh',
|
||||||
|
description:
|
||||||
|
'Ingest docs, wikis, and repos — keep assistants current with zero manual updates.',
|
||||||
|
icon: CloudTwoTone,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const journeys = [
|
||||||
|
{
|
||||||
|
name: 'Engineer Launch',
|
||||||
|
steps: 'Access, environments, codebase tour, first PR, observability basics.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?auto=format&fit=crop&w=800&q=80',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Customer Success Ramp',
|
||||||
|
steps: 'Playbooks, product scenarios, objection handling, success plans, CRM hygiene.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=900&q=80',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Product Discovery',
|
||||||
|
steps: 'Interview templates, JTBD mapping, experiment cards, roadmap debates.',
|
||||||
|
image: 'https://images.unsplash.com/photo-1483478550801-ceba5fe50e8e?auto=format&fit=crop&w=900&q=80',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const testimonials = [
|
||||||
|
{
|
||||||
|
name: 'Amira Chen',
|
||||||
|
role: 'VP Engineering, Nimbus',
|
||||||
|
quote: 'We cut onboarding from weeks to days. The guided flows and assessments keep everyone aligned.',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=200&q=80',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Luis Ortega',
|
||||||
|
role: 'Head of Success, Calypso',
|
||||||
|
quote: 'Playbooks stay fresh automatically. New CSMs ship value on day one.',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=200&q=80',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const logos = [
|
||||||
|
'https://dummyimage.com/120x40/111827/ffffff&text=Nova',
|
||||||
|
'https://dummyimage.com/120x40/1f2937/ffffff&text=Helio',
|
||||||
|
'https://dummyimage.com/120x40/111827/ffffff&text=Arcus',
|
||||||
|
'https://dummyimage.com/120x40/1f2937/ffffff&text=Vertex',
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="page">
|
||||||
|
<section class="hero">
|
||||||
|
<Row :gutter="32" :align="'middle'">
|
||||||
|
<Col :xs="24" :md="14">
|
||||||
|
<Typography.Title :level="1" class="hero-title">
|
||||||
|
Build agentic onboarding that feels bespoke to every role
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Paragraph class="hero-sub">
|
||||||
|
AI-led workflows, assessments, and knowledge delivery that adapt to your
|
||||||
|
stack, your rituals, and your teams - so every new hire ships confidently,
|
||||||
|
faster.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Space>
|
||||||
|
<RouterLink to="/about">
|
||||||
|
<Button type="primary" size="large">Learn More</Button>
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink to="/onboarding">
|
||||||
|
<Button size="large">See Onboarding Flows</Button>
|
||||||
|
</RouterLink>
|
||||||
|
</Space>
|
||||||
|
<Divider />
|
||||||
|
<Row :gutter="16">
|
||||||
|
<Col v-for="stat in stats" :key="stat.title" :xs="24" :sm="8">
|
||||||
|
<Card :bordered="false" class="stat-card" hoverable>
|
||||||
|
<Statistic :title="stat.title" :value="stat.value" />
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
<Col :xs="24" :md="10">
|
||||||
|
<Card class="hero-card" hoverable :cover="null">
|
||||||
|
<img :src="heroImage" alt="Team collaborating" class="hero-img" />
|
||||||
|
<div class="hero-overlay">Adaptive AI playbooks</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="trusted">
|
||||||
|
<Typography.Text type="secondary">Trusted by modern teams</Typography.Text>
|
||||||
|
<div class="logo-row">
|
||||||
|
<img v-for="logo in logos" :key="logo" :src="logo" alt="logo" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="features">
|
||||||
|
<Typography.Title :level="2">Everything you need to ramp faster</Typography.Title>
|
||||||
|
<Row :gutter="16">
|
||||||
|
<Col v-for="feature in features" :key="feature.title" :xs="24" :md="8">
|
||||||
|
<Card hoverable class="feature-card">
|
||||||
|
<feature.icon two-tone-color="#8b5cf6" style="font-size: 28px" />
|
||||||
|
<Typography.Title :level="4">{{ feature.title }}</Typography.Title>
|
||||||
|
<Typography.Paragraph>{{ feature.description }}</Typography.Paragraph>
|
||||||
|
<Tag color="purple">Live</Tag>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="journeys">
|
||||||
|
<Typography.Title :level="2">Prebuilt journeys, tailored in minutes</Typography.Title>
|
||||||
|
<Row :gutter="16">
|
||||||
|
<Col v-for="journey in journeys" :key="journey.name" :xs="24" :md="8">
|
||||||
|
<Card hoverable class="journey-card">
|
||||||
|
<template #cover>
|
||||||
|
<img :alt="journey.name" :src="journey.image" />
|
||||||
|
</template>
|
||||||
|
<Typography.Title :level="4">{{ journey.name }}</Typography.Title>
|
||||||
|
<Typography.Text>{{ journey.steps }}</Typography.Text>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="testimonials">
|
||||||
|
<Typography.Title :level="2">What teams are saying</Typography.Title>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
style="display: block; text-align: center; margin-bottom: 1rem"
|
||||||
|
>
|
||||||
|
(Demo testimonials, real feedback coming soon...)
|
||||||
|
</Typography.Text>
|
||||||
|
<Carousel autoplay dot-position="bottom">
|
||||||
|
<div v-for="t in testimonials" :key="t.name" class="testimonial-slide">
|
||||||
|
<Card :bordered="false" class="testimonial-card">
|
||||||
|
<Typography.Paragraph>“{{ t.quote }}”</Typography.Paragraph>
|
||||||
|
<Space>
|
||||||
|
<Avatar :src="t.avatar" size="large" />
|
||||||
|
<div>
|
||||||
|
<div class="t-name">{{ t.name }}</div>
|
||||||
|
<Typography.Text type="secondary">{{ t.role }}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Carousel>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
.hero-title {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.hero-sub {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
.hero-card {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.hero-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 280px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.hero-overlay {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
color: #8b5cf6;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.stat-card {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
}
|
||||||
|
.trusted {
|
||||||
|
text-align: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
.logo-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
.logo-row img {
|
||||||
|
opacity: 0.8;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.features {
|
||||||
|
margin: 2.5rem 0;
|
||||||
|
}
|
||||||
|
.feature-card {
|
||||||
|
height: 100%;
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.journeys {
|
||||||
|
margin: 2.5rem 0;
|
||||||
|
}
|
||||||
|
.journey-card {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.testimonials {
|
||||||
|
margin: 2.5rem 0;
|
||||||
|
}
|
||||||
|
.testimonial-slide {
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
.testimonial-card {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page {
|
||||||
|
padding: 1.25rem 1rem 2.5rem;
|
||||||
|
}
|
||||||
|
.hero-img {
|
||||||
|
height: 220px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
site/src/views/InviteAccept.vue
Normal file
92
site/src/views/InviteAccept.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { Card, Button, Spin, message, Result } from 'ant-design-vue'
|
||||||
|
import { apiClient, isAxiosError, API } from '../router/api'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const token = route.params.token as string
|
||||||
|
const loading = ref(false)
|
||||||
|
const accepting = ref(false)
|
||||||
|
const accepted = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
const acceptInvite = async () => {
|
||||||
|
accepting.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post<{ message: string; success: boolean; uuid: string }>(
|
||||||
|
API.organizationJoin(token),
|
||||||
|
)
|
||||||
|
message.success(response.data?.message || 'Successfully joined organization')
|
||||||
|
accepted.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
if (response.data?.uuid) router.push(`/organization/${response.data.uuid}`)
|
||||||
|
else router.push('/')
|
||||||
|
}, 1500)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to accept invite:', err)
|
||||||
|
if (isAxiosError(err)) {
|
||||||
|
const respErr = err.response?.data?.error || err.response?.data?.detail
|
||||||
|
error.value = respErr ? String(respErr) : 'Failed to accept invite'
|
||||||
|
} else {
|
||||||
|
error.value = 'Failed to accept invite'
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
accepting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
acceptInvite()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Spin :spinning="loading" tip="Loading invite...">
|
||||||
|
<Card class="panel" :bordered="false">
|
||||||
|
<div v-if="error">
|
||||||
|
<Result status="error" :title="error">
|
||||||
|
<template #extra>
|
||||||
|
<Button type="primary" @click="router.push('/')">Go Home</Button>
|
||||||
|
</template>
|
||||||
|
</Result>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="accepted">
|
||||||
|
<Result
|
||||||
|
status="success"
|
||||||
|
title="Successfully Joined Organization"
|
||||||
|
sub-title="Redirecting to organization page..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-info {
|
||||||
|
background: #1f2937;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
117
site/src/views/LoginView.vue
Normal file
117
site/src/views/LoginView.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, computed, onMounted, ref } from 'vue'
|
||||||
|
import type { VNodeRef } from 'vue'
|
||||||
|
defineOptions({ name: 'LoginView' })
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { Card, Typography, Form, Input, Button, message } from 'ant-design-vue'
|
||||||
|
import { useUserStore } from '../stores/userStore'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const loading = computed(() => userStore.loading)
|
||||||
|
|
||||||
|
const formRef = ref<VNodeRef | null>(null)
|
||||||
|
|
||||||
|
const formState = reactive({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
await userStore.login(formState.email, formState.password)
|
||||||
|
message.success('Login successful')
|
||||||
|
const redirect = (route.query.redirect as string) || '/organization'
|
||||||
|
router.push(redirect)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
let errorMsg = 'Login failed'
|
||||||
|
if (userStore.error) {
|
||||||
|
errorMsg = userStore.error
|
||||||
|
} else if (typeof error === 'string') {
|
||||||
|
errorMsg = error
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
errorMsg = error.message || 'Login failed'
|
||||||
|
} else if (typeof error === 'object' && error !== null) {
|
||||||
|
const errObj = error as { [k: string]: unknown }
|
||||||
|
const maybeResp = errObj?.response as unknown
|
||||||
|
if (typeof maybeResp === 'object' && maybeResp !== null) {
|
||||||
|
const respObj = maybeResp as { data?: unknown }
|
||||||
|
const data = respObj.data
|
||||||
|
if (typeof data === 'object' && data !== null) {
|
||||||
|
const detail = (data as { detail?: unknown }).detail
|
||||||
|
const msg = (data as { message?: unknown }).message
|
||||||
|
if (typeof detail === 'string') {
|
||||||
|
errorMsg = detail
|
||||||
|
} else if (typeof msg === 'string') {
|
||||||
|
errorMsg = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message.error(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await userStore.fetchSession()
|
||||||
|
if (userStore.isAuthenticated) {
|
||||||
|
const redirect = (route.query.redirect as string) || '/organization'
|
||||||
|
router.replace(redirect)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="auth-page">
|
||||||
|
<Card class="panel" :bordered="false">
|
||||||
|
<Typography.Title :level="3">Login</Typography.Title>
|
||||||
|
<Form :ref="formRef" layout="vertical" :model="formState" @finish="submit">
|
||||||
|
<Form.Item
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
:rules="[
|
||||||
|
{ required: true, message: 'Enter your email' },
|
||||||
|
{
|
||||||
|
type: 'email',
|
||||||
|
message: 'Please enter a valid email',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-model:value="formState.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email address"
|
||||||
|
:disabled="loading"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
:rules="[{ required: true, message: 'Enter your password' }]"
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
v-model:value="formState.password"
|
||||||
|
placeholder="Password"
|
||||||
|
:disabled="loading"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Button type="primary" html-type="submit" block :loading="loading">Login</Button>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auth-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue