Revised all files to reduce bloat + optimized workflow

This commit is contained in:
Viswamedha Nalabotu 2026-02-26 01:32:04 +00:00
parent af1ca55611
commit dcc04ca6ca
111 changed files with 17862 additions and 0 deletions

38
.dockerignore Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig"
]
}

103
README.md Normal file
View 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
View file

View file

52
apps/accounts/admin.py Normal file
View 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
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.accounts'

View 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
View 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)

View 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',
},
),
]

View file

@ -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'),
),
]

View file

19
apps/accounts/mixins.py Normal file
View 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
View 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"}
)

View 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
View 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)

View file

35
apps/knowledge/admin.py Normal file
View 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
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class KnowledgeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.knowledge'

View 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',
},
),
]

View file

75
apps/knowledge/models.py Normal file
View 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)

View 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
View 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

View 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()

View file

53
apps/onboarding/admin.py Normal file
View 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
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class OnboardingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.onboarding'

View 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
View 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}

View 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'],
},
),
]

View file

@ -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'),
),
]

View 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'),
),
]

View file

85
apps/onboarding/models.py Normal file
View 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

View 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()),
]

View 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
View 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()

View 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"]

View 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
View 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

View 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:

View 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"]

View 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"]

View 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"]

View 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"]

View 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

View 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
View file

@ -0,0 +1,3 @@
from .celery import app as celery_app
__all__ = ('celery_app',)

18
config/api.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()

View 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\\\\nHeres 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, youll 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 Reddits r/computervision or Stack Overflow.\\\\n- Experiment with cutting-edge models and techniques in computer vision.\\\\n\\\\n---\\\\n\\\\nWith practice and persistence, youll 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\\nHeres 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, youll 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 Reddits r/computervision or Stack Overflow.\\n- Experiment with cutting-edge models and techniques in computer vision.\\n\\n---\\n\\nWith practice and persistence, youll 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\\nHeres 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, youll 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 Reddits r/computervision or Stack Overflow.\\n- Experiment with cutting-edge models and techniques in computer vision.\\n\\n---\\n\\nWith practice and persistence, youll 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
}

File diff suppressed because it is too large Load diff

View 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
}

View 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
}

View 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 Readers Viewpoint* is probably one of the most ambitious epics Ive 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** hes been watching. In trying to fix his own story, he triggers a new one. He unknowingly causes the very events that lead to KDJs 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** hes been watching. In trying to fix his own story, he triggers a new one. He unknowingly causes the very events that lead to KDJs 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
View 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
View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

50
site/package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View 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,
}
})

View 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
View file

@ -0,0 +1,6 @@
export type AgentEvent = {
type: string
timestamp: Date
message?: string
content?: unknown
}

View 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
}

View 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
View 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
}

View 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>
&nbsp;{{ 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>

View 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>

View 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
View 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>

View 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>

View 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