feat: versioning, feat: in app configueration, feat: single exe, feat: reasoning, action: inital version, fix: config saving
Build Release EXE / build-windows-exe (release) Successful in 1m5s
Build Release EXE / build-windows-exe (release) Successful in 1m5s
This commit is contained in:
+1
-1
@@ -5,6 +5,6 @@ UEX_BASE_URL=https://api.uexcorp.space/2.0
|
||||
UEX_SECRET_KEY=
|
||||
UEX_BEARER_TOKEN=
|
||||
TRADERAI_USER_NAME=
|
||||
TRADERAI_MEMORY_PATH=data/traderai.sqlite3
|
||||
TRADERAI_MEMORY_PATH=
|
||||
UEX_NOTIFICATION_POLL_SECONDS=60
|
||||
REQUIRE_WRITE_APPROVAL=true
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
name: Build Release EXE
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-windows-exe:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build dependencies
|
||||
shell: pwsh
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
|
||||
- name: Build TraderAI.exe
|
||||
shell: pwsh
|
||||
run: |
|
||||
pyinstaller TraderAI.spec --noconfirm
|
||||
if (-not (Test-Path -LiteralPath "dist\TraderAI.exe")) {
|
||||
throw "dist\TraderAI.exe was not created."
|
||||
}
|
||||
|
||||
- name: Attach EXE to release
|
||||
shell: pwsh
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$event = Get-Content -LiteralPath $env:GITHUB_EVENT_PATH -Raw | ConvertFrom-Json
|
||||
$releaseId = $event.release.id
|
||||
if (-not $releaseId) {
|
||||
throw "Release id was not present in the release event payload."
|
||||
}
|
||||
|
||||
$token = $env:RELEASE_TOKEN
|
||||
if ([string]::IsNullOrWhiteSpace($token)) {
|
||||
$token = $env:GITEA_TOKEN
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($token)) {
|
||||
throw "Set a RELEASE_TOKEN secret or enable the built-in GITHUB_TOKEN for Actions."
|
||||
}
|
||||
|
||||
$apiUrl = $env:GITHUB_API_URL
|
||||
if ([string]::IsNullOrWhiteSpace($apiUrl)) {
|
||||
$apiUrl = "$($env:GITHUB_SERVER_URL.TrimEnd('/'))/api/v1"
|
||||
}
|
||||
|
||||
$repoParts = $env:GITHUB_REPOSITORY.Split("/", 2)
|
||||
if ($repoParts.Length -ne 2) {
|
||||
throw "GITHUB_REPOSITORY must look like owner/repo. Value: $env:GITHUB_REPOSITORY"
|
||||
}
|
||||
|
||||
$owner = [uri]::EscapeDataString($repoParts[0])
|
||||
$repo = [uri]::EscapeDataString($repoParts[1])
|
||||
$assetPath = Resolve-Path -LiteralPath "dist\TraderAI.exe"
|
||||
$uploadUrl = "$apiUrl/repos/$owner/$repo/releases/$releaseId/assets?name=TraderAI.exe"
|
||||
|
||||
Invoke-RestMethod `
|
||||
-Method Post `
|
||||
-Uri $uploadUrl `
|
||||
-Headers @{ Authorization = "token $token" } `
|
||||
-Form @{ attachment = Get-Item -LiteralPath $assetPath }
|
||||
@@ -19,6 +19,7 @@ dist/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
pip-wheel-metadata/
|
||||
.playwright-mcp/
|
||||
|
||||
# Test and coverage output
|
||||
.pytest_cache/
|
||||
|
||||
@@ -36,6 +36,18 @@ Local Ollama-powered chat for UEX marketplace workflows.
|
||||
|
||||
Ollama runs locally at `http://localhost:11434` by default. This app talks to Ollama's native chat API with tool schemas, then executes approved UEX calls in the FastAPI backend. `OLLAMA_NUM_CTX` controls the per-request Ollama context window; `64000` is the default because Ollama recommends at least 64k tokens for agent-style workflows when hardware allows it.
|
||||
|
||||
## Releases And Updates
|
||||
|
||||
Change the app version before cutting a release:
|
||||
|
||||
```powershell
|
||||
.\scripts\set_version.ps1 0.2.0
|
||||
```
|
||||
|
||||
Create a Gitea release with a matching tag such as `v0.2.0`. The release workflow builds `dist\TraderAI.exe` and attaches only that exe to the release.
|
||||
|
||||
The desktop app can check `https://git.hudsonriggs.systems/LambdaBankingConglomerate/TraderAI/releases` from Settings > Updates. When a newer release has a `TraderAI.exe` attachment, the packaged app downloads it to the user app data update folder, exits, replaces the current exe, and relaunches.
|
||||
|
||||
UEX marketplace posting and negotiation messages are guarded because they are account-affecting write actions. The model can draft them, but the UI approval button performs the final API call.
|
||||
|
||||
The assistant gets runtime context on every chat: current date/time, authenticated UEX identity when credentials are configured, remembered user profile, last interaction time, relevant memories, and recent conversation excerpts. It is instructed to prefer open/current marketplace data, avoid historical sale information unless explicitly requested, and treat UEX prices as in-game aUEC/UEC credits rather than real-world dollars. Memory is stored locally at `TRADERAI_MEMORY_PATH`.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
webview_datas, webview_binaries, webview_hiddenimports = collect_all("webview")
|
||||
|
||||
|
||||
a = Analysis(
|
||||
["traderai\\desktop.py"],
|
||||
pathex=[],
|
||||
binaries=webview_binaries,
|
||||
datas=[("web", "web"), *webview_datas],
|
||||
hiddenimports=[
|
||||
*webview_hiddenimports,
|
||||
"uvicorn.logging",
|
||||
"uvicorn.loops",
|
||||
"uvicorn.loops.auto",
|
||||
"uvicorn.protocols",
|
||||
"uvicorn.protocols.http",
|
||||
"uvicorn.protocols.http.auto",
|
||||
"uvicorn.protocols.websockets",
|
||||
"uvicorn.protocols.websockets.auto",
|
||||
"uvicorn.lifespan",
|
||||
"uvicorn.lifespan.on",
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name="TraderAI.Debug",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
icon="web\\art\\LBC_Logo.ico",
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
webview_datas, webview_binaries, webview_hiddenimports = collect_all("webview")
|
||||
|
||||
|
||||
a = Analysis(
|
||||
["traderai\\desktop.py"],
|
||||
pathex=[],
|
||||
binaries=webview_binaries,
|
||||
datas=[("web", "web"), *webview_datas],
|
||||
hiddenimports=[
|
||||
*webview_hiddenimports,
|
||||
"uvicorn.logging",
|
||||
"uvicorn.loops",
|
||||
"uvicorn.loops.auto",
|
||||
"uvicorn.protocols",
|
||||
"uvicorn.protocols.http",
|
||||
"uvicorn.protocols.http.auto",
|
||||
"uvicorn.protocols.websockets",
|
||||
"uvicorn.protocols.websockets.auto",
|
||||
"uvicorn.lifespan",
|
||||
"uvicorn.lifespan.on",
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name="TraderAI",
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
icon="web\\art\\LBC_Logo.ico",
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
+9
-1
@@ -1,12 +1,13 @@
|
||||
[project]
|
||||
name = "traderai"
|
||||
version = "0.1.0"
|
||||
version = "0.0.1"
|
||||
description = "Local Ollama-powered assistant for UEX marketplace workflows."
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"apscheduler>=3.10.4",
|
||||
"fastapi>=0.115.0",
|
||||
"httpx>=0.27.0",
|
||||
"pywebview>=5.4",
|
||||
"pydantic>=2.8.0",
|
||||
"pydantic-settings>=2.4.0",
|
||||
"python-dotenv>=1.0.1",
|
||||
@@ -16,14 +17,21 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pyinstaller>=6.11.0",
|
||||
"pytest>=8.3.0",
|
||||
"pytest-asyncio>=0.23.8",
|
||||
"respx>=0.21.1",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
traderai-desktop = "traderai.desktop:main"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["."]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["traderai*"]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Root = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $Root
|
||||
|
||||
$Python = Join-Path $Root ".venv\Scripts\python.exe"
|
||||
if (-not (Test-Path $Python)) {
|
||||
$Python = "python"
|
||||
}
|
||||
|
||||
& $Python -m ensurepip --upgrade
|
||||
& $Python -m pip install -e ".[dev]"
|
||||
& $Python -m PyInstaller --clean "TraderAI.spec"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Built dist\TraderAI.exe"
|
||||
@@ -0,0 +1,29 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidatePattern('^v?\d+\.\d+\.\d+([-.+][0-9A-Za-z.-]+)?$')]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")
|
||||
$cleanVersion = $Version.TrimStart("v")
|
||||
|
||||
$pyprojectPath = Join-Path $repoRoot "pyproject.toml"
|
||||
$versionPath = Join-Path $repoRoot "traderai\version.py"
|
||||
$lockPath = Join-Path $repoRoot "uv.lock"
|
||||
|
||||
$pyproject = Get-Content -LiteralPath $pyprojectPath -Raw
|
||||
$pyproject = $pyproject -replace '(?m)^version = "[^"]+"', "version = `"$cleanVersion`""
|
||||
Set-Content -LiteralPath $pyprojectPath -Value $pyproject -Encoding UTF8
|
||||
|
||||
$versionModule = Get-Content -LiteralPath $versionPath -Raw
|
||||
$versionModule = $versionModule -replace '__version__ = "[^"]+"', "__version__ = `"$cleanVersion`""
|
||||
Set-Content -LiteralPath $versionPath -Value $versionModule -Encoding UTF8
|
||||
|
||||
if (Test-Path -LiteralPath $lockPath) {
|
||||
$lock = Get-Content -LiteralPath $lockPath -Raw
|
||||
$lock = $lock -replace '(?s)(name = "traderai"\s+version = ")[^"]+(")', "`${1}$cleanVersion`${2}"
|
||||
Set-Content -LiteralPath $lockPath -Value $lock -Encoding UTF8
|
||||
}
|
||||
|
||||
Write-Host "TraderAI version set to $cleanVersion"
|
||||
+126
-4
@@ -1,11 +1,64 @@
|
||||
from functools import lru_cache
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import Field
|
||||
from functools import lru_cache
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
CONFIG_FIELDS: dict[str, dict[str, Any]] = {
|
||||
"ollama_base_url": {"env": "OLLAMA_BASE_URL", "type": "string", "secret": False},
|
||||
"ollama_model": {"env": "OLLAMA_MODEL", "type": "string", "secret": False},
|
||||
"ollama_num_ctx": {"env": "OLLAMA_NUM_CTX", "type": "integer", "secret": False},
|
||||
"uex_base_url": {"env": "UEX_BASE_URL", "type": "string", "secret": False},
|
||||
"uex_secret_key": {"env": "UEX_SECRET_KEY", "type": "string", "secret": True},
|
||||
"uex_bearer_token": {"env": "UEX_BEARER_TOKEN", "type": "string", "secret": True},
|
||||
"traderai_user_name": {"env": "TRADERAI_USER_NAME", "type": "string", "secret": False},
|
||||
"traderai_memory_path": {"env": "TRADERAI_MEMORY_PATH", "type": "string", "secret": False},
|
||||
"uex_notification_poll_seconds": {"env": "UEX_NOTIFICATION_POLL_SECONDS", "type": "integer", "secret": False},
|
||||
"require_write_approval": {"env": "REQUIRE_WRITE_APPROVAL", "type": "boolean", "secret": False},
|
||||
}
|
||||
|
||||
|
||||
def app_data_dir() -> Path:
|
||||
if sys.platform == "win32":
|
||||
root = os.environ.get("LOCALAPPDATA")
|
||||
if root:
|
||||
return Path(root) / "TraderAI"
|
||||
return Path.home() / ".traderai"
|
||||
|
||||
|
||||
def ensure_app_data_dir() -> Path:
|
||||
path = app_data_dir()
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def user_config_path() -> Path:
|
||||
return ensure_app_data_dir() / ".env"
|
||||
|
||||
|
||||
def default_memory_path() -> Path:
|
||||
return ensure_app_data_dir() / "traderai.sqlite3"
|
||||
|
||||
|
||||
def log_path() -> Path:
|
||||
return ensure_app_data_dir() / "TraderAI.log"
|
||||
|
||||
|
||||
def edge_profile_dir() -> Path:
|
||||
return ensure_app_data_dir() / "EdgeProfile"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=(".env", str(user_config_path())),
|
||||
env_file_encoding="utf-8",
|
||||
)
|
||||
|
||||
ollama_base_url: str = "http://localhost:11434"
|
||||
ollama_model: str = "qwen3.5:9b"
|
||||
@@ -14,11 +67,80 @@ class Settings(BaseSettings):
|
||||
uex_secret_key: str | None = Field(default=None)
|
||||
uex_bearer_token: str | None = Field(default=None)
|
||||
traderai_user_name: str | None = Field(default=None)
|
||||
traderai_memory_path: str = "data/traderai.sqlite3"
|
||||
traderai_memory_path: str = Field(default_factory=lambda: str(default_memory_path()))
|
||||
uex_notification_poll_seconds: int = 60
|
||||
require_write_approval: bool = True
|
||||
|
||||
@field_validator("uex_secret_key", "uex_bearer_token", "traderai_user_name", mode="before")
|
||||
@classmethod
|
||||
def _blank_optional(cls, value: Any) -> Any:
|
||||
return None if value == "" else value
|
||||
|
||||
@field_validator("traderai_memory_path", mode="before")
|
||||
@classmethod
|
||||
def _blank_memory_path(cls, value: Any) -> Any:
|
||||
return str(default_memory_path()) if value == "" or value is None else value
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
|
||||
def settings_payload(settings: Settings | None = None) -> dict[str, Any]:
|
||||
current = settings or get_settings()
|
||||
values = current.model_dump()
|
||||
return {
|
||||
"app_data_dir": str(ensure_app_data_dir()),
|
||||
"config_path": str(user_config_path()),
|
||||
"log_path": str(log_path()),
|
||||
"edge_profile_dir": str(edge_profile_dir()),
|
||||
"values": values,
|
||||
"fields": CONFIG_FIELDS,
|
||||
}
|
||||
|
||||
|
||||
def save_settings(values: dict[str, Any]) -> dict[str, Any]:
|
||||
current = get_settings().model_dump()
|
||||
next_values = dict(current)
|
||||
for key, value in values.items():
|
||||
if key not in CONFIG_FIELDS:
|
||||
continue
|
||||
next_values[key] = _coerce_value(key, value)
|
||||
|
||||
path = user_config_path()
|
||||
lines = [
|
||||
"# TraderAI desktop configuration",
|
||||
"# Saved by the app. Environment variables still override these values.",
|
||||
"",
|
||||
]
|
||||
for key, meta in CONFIG_FIELDS.items():
|
||||
value = next_values.get(key)
|
||||
lines.append(f"{meta['env']}={_env_value(value)}")
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
get_settings.cache_clear()
|
||||
return settings_payload(get_settings())
|
||||
|
||||
|
||||
def _coerce_value(key: str, value: Any) -> Any:
|
||||
field_type = CONFIG_FIELDS[key]["type"]
|
||||
if value == "":
|
||||
return None if key in {"uex_secret_key", "uex_bearer_token", "traderai_user_name"} else ""
|
||||
if field_type == "integer":
|
||||
return int(value)
|
||||
if field_type == "boolean":
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return str(value).strip().casefold() in {"1", "true", "yes", "on"}
|
||||
return str(value)
|
||||
|
||||
|
||||
def _env_value(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
text = str(value)
|
||||
if not text or any(char.isspace() for char in text) or "#" in text:
|
||||
return '"' + text.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
||||
return text
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from typing import NoReturn
|
||||
|
||||
import httpx
|
||||
import uvicorn
|
||||
|
||||
from traderai.config import edge_profile_dir, log_path
|
||||
|
||||
|
||||
def resource_path(*parts: str) -> Path:
|
||||
base = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent.parent))
|
||||
return base.joinpath(*parts)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
_chdir_to_app_dir()
|
||||
_log("TraderAI desktop starting")
|
||||
_log(f"cwd={Path.cwd()}")
|
||||
_log(f"executable={sys.executable}")
|
||||
_log(f"frozen={getattr(sys, 'frozen', False)} meipass={getattr(sys, '_MEIPASS', '')}")
|
||||
port = _select_port()
|
||||
url = f"http://127.0.0.1:{port}"
|
||||
_log(f"selected_url={url}")
|
||||
if _existing_server_ready(url):
|
||||
_log("existing TraderAI backend found; opening window")
|
||||
_open_window(url)
|
||||
return
|
||||
server_thread = threading.Thread(target=_run_server, args=(port,), daemon=True)
|
||||
server_thread.start()
|
||||
_log("backend thread started")
|
||||
_wait_for_server(url)
|
||||
_log("backend health check passed")
|
||||
_open_window(url)
|
||||
_log("webview closed")
|
||||
except Exception:
|
||||
_log("fatal startup error")
|
||||
_log(traceback.format_exc())
|
||||
raise
|
||||
|
||||
|
||||
def _chdir_to_app_dir() -> None:
|
||||
if getattr(sys, "frozen", False):
|
||||
os.chdir(Path(sys.executable).resolve().parent)
|
||||
|
||||
|
||||
def _select_port() -> int:
|
||||
preferred = int(os.getenv("TRADERAI_PORT", "8765"))
|
||||
if _port_available(preferred):
|
||||
return preferred
|
||||
_log(f"preferred port {preferred} is in use")
|
||||
return _free_port()
|
||||
|
||||
|
||||
def _port_available(port: int) -> bool:
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(("127.0.0.1", port))
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _free_port() -> int:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
return int(sock.getsockname()[1])
|
||||
|
||||
|
||||
def _existing_server_ready(url: str) -> bool:
|
||||
try:
|
||||
response = httpx.get(f"{url}/api/health", timeout=1)
|
||||
return response.status_code < 500 and response.headers.get("content-type", "").startswith("application/json")
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
|
||||
def _run_server(port: int) -> NoReturn:
|
||||
try:
|
||||
_log(f"backend starting on port {port}")
|
||||
from traderai.server import app
|
||||
|
||||
config = uvicorn.Config(
|
||||
app,
|
||||
host="127.0.0.1",
|
||||
port=port,
|
||||
log_level="info",
|
||||
log_config=None,
|
||||
lifespan="on",
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
server.run()
|
||||
_log("backend server stopped")
|
||||
raise SystemExit(0)
|
||||
except BaseException:
|
||||
_log("backend thread crashed")
|
||||
_log(traceback.format_exc())
|
||||
raise
|
||||
|
||||
|
||||
def _wait_for_server(url: str) -> None:
|
||||
deadline = time.monotonic() + 30
|
||||
last_error = ""
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
response = httpx.get(f"{url}/api/health", timeout=1)
|
||||
_log(f"health probe status={response.status_code}")
|
||||
if response.status_code < 500:
|
||||
return
|
||||
except httpx.HTTPError as exc:
|
||||
last_error = str(exc)
|
||||
_log(f"health probe failed: {last_error}")
|
||||
time.sleep(0.25)
|
||||
raise RuntimeError(f"TraderAI backend did not start within 30 seconds. {last_error}")
|
||||
|
||||
|
||||
def _open_window(url: str) -> None:
|
||||
mode = os.getenv("TRADERAI_DESKTOP_UI", "edge").casefold()
|
||||
_log(f"ui_mode={mode}")
|
||||
if mode == "webview":
|
||||
_open_webview(url)
|
||||
return
|
||||
if _open_edge_app(url):
|
||||
return
|
||||
_open_browser(url)
|
||||
|
||||
|
||||
def _open_webview(url: str) -> None:
|
||||
_log("importing pywebview")
|
||||
import webview
|
||||
|
||||
_log("creating pywebview window")
|
||||
webview.create_window(
|
||||
"TraderAI",
|
||||
url,
|
||||
width=1320,
|
||||
height=860,
|
||||
min_size=(980, 680),
|
||||
text_select=True,
|
||||
icon=str(resource_path("web", "art", "LBC_Logo.ico")),
|
||||
)
|
||||
_log("starting pywebview")
|
||||
webview.start(gui="edgechromium", debug=False)
|
||||
|
||||
|
||||
def _open_edge_app(url: str) -> bool:
|
||||
edge = _edge_path()
|
||||
if not edge:
|
||||
_log("msedge not found; falling back to default browser")
|
||||
return False
|
||||
profile_dir = edge_profile_dir()
|
||||
profile_dir.mkdir(parents=True, exist_ok=True)
|
||||
command = [
|
||||
str(edge),
|
||||
f"--app={url}",
|
||||
f"--user-data-dir={profile_dir}",
|
||||
"--new-window",
|
||||
"--no-first-run",
|
||||
"--disable-features=Translate",
|
||||
f"--app-icon={resource_path('web', 'art', 'LBC_Logo.ico')}",
|
||||
]
|
||||
_log(f"launching edge app: {' '.join(command)}")
|
||||
process = subprocess.Popen(command)
|
||||
_log(f"edge process id={process.pid}")
|
||||
time.sleep(2)
|
||||
if process.poll() is None:
|
||||
process.wait()
|
||||
_log("edge app process exited")
|
||||
return True
|
||||
_log(f"edge app process exited early code={process.returncode}; keeping backend alive")
|
||||
_keep_alive()
|
||||
return True
|
||||
|
||||
|
||||
def _open_browser(url: str) -> None:
|
||||
import webbrowser
|
||||
|
||||
_log(f"opening default browser at {url}")
|
||||
webbrowser.open(url)
|
||||
_keep_alive()
|
||||
|
||||
|
||||
def _keep_alive() -> None:
|
||||
_log("backend staying alive; close TraderAI from Task Manager if no app window owns this process")
|
||||
while True:
|
||||
time.sleep(60)
|
||||
|
||||
|
||||
def _edge_path() -> Path | None:
|
||||
edge = shutil.which("msedge")
|
||||
if edge:
|
||||
return Path(edge)
|
||||
candidates = [
|
||||
Path(os.environ.get("ProgramFiles", "")) / "Microsoft" / "Edge" / "Application" / "msedge.exe",
|
||||
Path(os.environ.get("ProgramFiles(x86)", "")) / "Microsoft" / "Edge" / "Application" / "msedge.exe",
|
||||
Path(os.environ.get("LocalAppData", "")) / "Microsoft" / "Edge" / "Application" / "msedge.exe",
|
||||
]
|
||||
for candidate in candidates:
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _log(message: str) -> None:
|
||||
try:
|
||||
log_path = _log_path()
|
||||
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
with log_path.open("a", encoding="utf-8") as file:
|
||||
file.write(f"[{timestamp}] {message}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _log_path() -> Path:
|
||||
return log_path()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+406
-2
@@ -1,8 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import FastAPI
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
@@ -10,11 +19,18 @@ from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
|
||||
from traderai.agent import OllamaAgent, OllamaUnavailable
|
||||
from traderai.config import save_settings, settings_payload
|
||||
from traderai.config import get_settings
|
||||
from traderai.memory import MemoryStore
|
||||
from traderai.scheduler import WakeScheduler
|
||||
from traderai.tools import ToolRegistry
|
||||
from traderai.uex_client import UEXClient
|
||||
from traderai.version import RELEASES_API_URL, RELEASES_URL, __version__
|
||||
|
||||
|
||||
def resource_path(*parts: str) -> Path:
|
||||
base = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent.parent))
|
||||
return base.joinpath(*parts)
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
@@ -29,6 +45,18 @@ class ClearMemoryRequest(BaseModel):
|
||||
include_outbox: bool = True
|
||||
|
||||
|
||||
class ConfigUpdateRequest(BaseModel):
|
||||
values: dict
|
||||
|
||||
|
||||
class OllamaModelRequest(BaseModel):
|
||||
model: str | None = None
|
||||
|
||||
|
||||
OLLAMA_DOWNLOAD_URL = "https://ollama.com/download/windows"
|
||||
UPDATE_ASSET_NAME = "TraderAI.exe"
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
settings = get_settings()
|
||||
memory = MemoryStore(settings.traderai_memory_path)
|
||||
@@ -47,7 +75,7 @@ def create_app() -> FastAPI:
|
||||
scheduler.bind_uex_notifications(uex, settings.uex_notification_poll_seconds)
|
||||
|
||||
app = FastAPI(title="TraderAI")
|
||||
static_dir = Path(__file__).resolve().parent.parent / "web"
|
||||
static_dir = resource_path("web")
|
||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||
|
||||
@app.on_event("startup")
|
||||
@@ -93,8 +121,128 @@ def create_app() -> FastAPI:
|
||||
"ollama": await agent.health(),
|
||||
"user": memory.get_profile(),
|
||||
"jobs": scheduler.list_jobs(),
|
||||
"app_data_dir": settings_payload()["app_data_dir"],
|
||||
"version": __version__,
|
||||
}
|
||||
|
||||
@app.get("/api/config")
|
||||
async def inspect_config() -> dict:
|
||||
return settings_payload()
|
||||
|
||||
@app.post("/api/config")
|
||||
async def update_config(request: ConfigUpdateRequest) -> dict:
|
||||
updated = save_settings(request.values)
|
||||
updated["restart_required"] = True
|
||||
updated["message"] = "Configuration saved. Restart TraderAI for all settings to take effect."
|
||||
return updated
|
||||
|
||||
@app.get("/api/ollama/status")
|
||||
async def ollama_status() -> dict:
|
||||
return await inspect_ollama()
|
||||
|
||||
@app.post("/api/ollama/launch")
|
||||
async def launch_ollama() -> dict:
|
||||
command = ollama_launch_command()
|
||||
if not command:
|
||||
raise HTTPException(status_code=404, detail="Ollama is not installed or was not found on PATH.")
|
||||
try:
|
||||
popen_hidden(command)
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Could not launch Ollama: {exc}") from exc
|
||||
status = await inspect_ollama()
|
||||
status["message"] = "Ollama launch requested."
|
||||
return status
|
||||
|
||||
@app.post("/api/ollama/pull")
|
||||
async def pull_ollama_model(request: OllamaModelRequest) -> dict:
|
||||
settings_now = get_settings()
|
||||
model = (request.model or settings_now.ollama_model).strip()
|
||||
if not model:
|
||||
raise HTTPException(status_code=400, detail="No Ollama model is configured.")
|
||||
cli = find_ollama_cli()
|
||||
if not cli:
|
||||
raise HTTPException(status_code=404, detail="Ollama CLI was not found.")
|
||||
try:
|
||||
popen_hidden([str(cli), "pull", model])
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Could not start model install: {exc}") from exc
|
||||
status = await inspect_ollama()
|
||||
status["message"] = f"Started installing model {model}."
|
||||
return status
|
||||
|
||||
@app.post("/api/ollama/install")
|
||||
async def install_ollama() -> dict:
|
||||
winget = shutil.which("winget")
|
||||
if not winget:
|
||||
return {
|
||||
"started": False,
|
||||
"message": "winget is not available on this system. Open the download page instead.",
|
||||
"download_url": OLLAMA_DOWNLOAD_URL,
|
||||
}
|
||||
try:
|
||||
popen_hidden(
|
||||
[
|
||||
winget,
|
||||
"install",
|
||||
"-e",
|
||||
"--id",
|
||||
"Ollama.Ollama",
|
||||
"--accept-package-agreements",
|
||||
"--accept-source-agreements",
|
||||
]
|
||||
)
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Could not start Ollama install: {exc}") from exc
|
||||
return {"started": True, "message": "Started Ollama install with winget.", "download_url": OLLAMA_DOWNLOAD_URL}
|
||||
|
||||
@app.post("/api/ollama/download")
|
||||
async def download_ollama() -> dict:
|
||||
webbrowser.open(OLLAMA_DOWNLOAD_URL)
|
||||
return {"opened": True, "download_url": OLLAMA_DOWNLOAD_URL, "message": "Opened the Ollama download page."}
|
||||
|
||||
@app.get("/api/update/check")
|
||||
async def check_update() -> dict:
|
||||
return await inspect_update()
|
||||
|
||||
@app.post("/api/update/install")
|
||||
async def install_update() -> dict:
|
||||
update = await inspect_update()
|
||||
if not update["available"]:
|
||||
return {**update, "message": "TraderAI is already up to date."}
|
||||
if not getattr(sys, "frozen", False):
|
||||
return {
|
||||
**update,
|
||||
"started": False,
|
||||
"message": "Update download is available, but self-update only runs from the packaged exe.",
|
||||
}
|
||||
|
||||
asset_url = update.get("asset_download_url")
|
||||
if not asset_url:
|
||||
raise HTTPException(status_code=404, detail="The latest release does not include TraderAI.exe.")
|
||||
|
||||
downloaded = await download_update_asset(asset_url, update["latest_version"])
|
||||
script = write_update_script(downloaded, Path(sys.executable))
|
||||
updater_command = [
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
str(script),
|
||||
"-ProcessId",
|
||||
str(os.getpid()),
|
||||
"-Source",
|
||||
str(downloaded),
|
||||
"-Target",
|
||||
str(Path(sys.executable)),
|
||||
]
|
||||
updater_kwargs: dict[str, Any] = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL}
|
||||
if sys.platform == "win32":
|
||||
updater_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
||||
subprocess.Popen(updater_command, **updater_kwargs)
|
||||
threading.Thread(target=exit_after_update_response, daemon=True).start()
|
||||
return {**update, "started": True, "message": "Update downloaded. TraderAI will restart into the new version."}
|
||||
|
||||
@app.post("/api/chat")
|
||||
async def chat(request: ChatRequest) -> dict:
|
||||
try:
|
||||
@@ -152,4 +300,260 @@ def create_app() -> FastAPI:
|
||||
return app
|
||||
|
||||
|
||||
async def inspect_ollama() -> dict[str, Any]:
|
||||
settings = get_settings()
|
||||
executable = find_ollama_executable()
|
||||
cli = find_ollama_cli()
|
||||
models: list[str] = []
|
||||
online = False
|
||||
detail = ""
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=3) as client:
|
||||
response = await client.get(f"{settings.ollama_base_url.rstrip('/')}/api/tags")
|
||||
response.raise_for_status()
|
||||
body = response.json()
|
||||
online = True
|
||||
models = [item.get("name") or item.get("model") for item in body.get("models", [])]
|
||||
models = [model for model in models if model]
|
||||
except (httpx.HTTPError, ValueError) as exc:
|
||||
detail = str(exc)
|
||||
|
||||
installed = bool(executable or cli)
|
||||
model_available = settings.ollama_model in models
|
||||
return {
|
||||
"installed": installed,
|
||||
"running": online,
|
||||
"online": online,
|
||||
"model_available": model_available,
|
||||
"configured_model": settings.ollama_model,
|
||||
"base_url": settings.ollama_base_url,
|
||||
"num_ctx": settings.ollama_num_ctx,
|
||||
"models": models,
|
||||
"executable": str(executable) if executable else None,
|
||||
"cli": str(cli) if cli else None,
|
||||
"can_auto_install": bool(shutil.which("winget")),
|
||||
"download_url": OLLAMA_DOWNLOAD_URL,
|
||||
"message": ollama_status_message(installed, online, model_available, settings.ollama_model),
|
||||
"detail": detail,
|
||||
}
|
||||
|
||||
|
||||
def ollama_status_message(installed: bool, running: bool, model_available: bool, model: str) -> str:
|
||||
if not installed:
|
||||
return "Ollama is not installed."
|
||||
if not running:
|
||||
return "Ollama is installed but not running."
|
||||
if not model_available:
|
||||
return f'Ollama is running, but model "{model}" is not installed.'
|
||||
return "Ollama is ready."
|
||||
|
||||
|
||||
def find_ollama_executable() -> Path | None:
|
||||
candidates = [
|
||||
shutil.which("ollama"),
|
||||
os.environ.get("OLLAMA_EXE"),
|
||||
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Programs", "Ollama", "Ollama.exe"),
|
||||
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Programs", "Ollama", "ollama.exe"),
|
||||
os.path.join(os.environ.get("ProgramFiles", ""), "Ollama", "Ollama.exe"),
|
||||
os.path.join(os.environ.get("ProgramFiles", ""), "Ollama", "ollama.exe"),
|
||||
os.path.join(os.environ.get("ProgramFiles(x86)", ""), "Ollama", "Ollama.exe"),
|
||||
os.path.join(os.environ.get("ProgramFiles(x86)", ""), "Ollama", "ollama.exe"),
|
||||
]
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
path = Path(candidate)
|
||||
if path.exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def find_ollama_cli() -> Path | None:
|
||||
candidates = [
|
||||
shutil.which("ollama"),
|
||||
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Programs", "Ollama", "ollama.exe"),
|
||||
os.path.join(os.environ.get("ProgramFiles", ""), "Ollama", "ollama.exe"),
|
||||
os.path.join(os.environ.get("ProgramFiles(x86)", ""), "Ollama", "ollama.exe"),
|
||||
]
|
||||
for candidate in candidates:
|
||||
if not candidate:
|
||||
continue
|
||||
path = Path(candidate)
|
||||
if path.exists():
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def ollama_launch_command() -> list[str] | None:
|
||||
executable = find_ollama_executable()
|
||||
if not executable:
|
||||
return None
|
||||
if executable.name == "Ollama.exe":
|
||||
return [str(executable)]
|
||||
return [str(executable), "serve"]
|
||||
|
||||
|
||||
def popen_hidden(command: list[str]) -> subprocess.Popen:
|
||||
kwargs: dict[str, Any] = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL}
|
||||
if sys.platform == "win32":
|
||||
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
||||
return subprocess.Popen(command, **kwargs)
|
||||
|
||||
|
||||
async def inspect_update() -> dict[str, Any]:
|
||||
try:
|
||||
latest = await latest_release()
|
||||
except (httpx.HTTPError, ValueError) as exc:
|
||||
return {
|
||||
"current_version": __version__,
|
||||
"latest_version": None,
|
||||
"available": False,
|
||||
"release_url": RELEASES_URL,
|
||||
"message": f"Could not check releases: {exc}",
|
||||
}
|
||||
if not latest:
|
||||
return {
|
||||
"current_version": __version__,
|
||||
"latest_version": None,
|
||||
"available": False,
|
||||
"release_url": RELEASES_URL,
|
||||
"message": "No releases were found.",
|
||||
}
|
||||
|
||||
latest_version = normalize_version(latest.get("tag_name") or latest.get("name") or "")
|
||||
asset = release_asset(latest, UPDATE_ASSET_NAME)
|
||||
available = latest_version is not None and compare_versions(latest_version, __version__) > 0
|
||||
return {
|
||||
"current_version": __version__,
|
||||
"latest_version": latest_version,
|
||||
"available": available,
|
||||
"release_name": latest.get("name") or latest.get("tag_name"),
|
||||
"release_url": latest.get("html_url") or RELEASES_URL,
|
||||
"asset_name": asset.get("name") if asset else None,
|
||||
"asset_download_url": asset.get("browser_download_url") if asset else None,
|
||||
"packaged": bool(getattr(sys, "frozen", False)),
|
||||
"message": update_message(available, latest_version, bool(asset)),
|
||||
}
|
||||
|
||||
|
||||
async def latest_release() -> dict[str, Any] | None:
|
||||
async with httpx.AsyncClient(timeout=10, follow_redirects=True) as client:
|
||||
response = await client.get(RELEASES_API_URL)
|
||||
response.raise_for_status()
|
||||
releases = response.json()
|
||||
if not isinstance(releases, list):
|
||||
return None
|
||||
candidates = [
|
||||
release
|
||||
for release in releases
|
||||
if isinstance(release, dict) and not release.get("draft") and not release.get("prerelease")
|
||||
]
|
||||
if not candidates:
|
||||
candidates = [release for release in releases if isinstance(release, dict) and not release.get("draft")]
|
||||
if not candidates:
|
||||
return None
|
||||
return max(candidates, key=lambda release: version_parts(str(release.get("tag_name") or release.get("name") or "0")))
|
||||
|
||||
|
||||
def release_asset(release: dict[str, Any], name: str) -> dict[str, Any] | None:
|
||||
assets = release.get("assets") or []
|
||||
if not isinstance(assets, list):
|
||||
return None
|
||||
for asset in assets:
|
||||
if isinstance(asset, dict) and str(asset.get("name", "")).casefold() == name.casefold():
|
||||
return asset
|
||||
for asset in assets:
|
||||
download_url = str(asset.get("browser_download_url", "")) if isinstance(asset, dict) else ""
|
||||
if download_url.casefold().endswith(f"/{name.casefold()}"):
|
||||
return asset
|
||||
return None
|
||||
|
||||
|
||||
async def download_update_asset(url: str, version: str) -> Path:
|
||||
updates_dir = Path(settings_payload()["app_data_dir"]) / "updates"
|
||||
updates_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = updates_dir / f"TraderAI-{version}.exe"
|
||||
async with httpx.AsyncClient(timeout=120, follow_redirects=True) as client:
|
||||
async with client.stream("GET", url) as response:
|
||||
response.raise_for_status()
|
||||
with path.open("wb") as file:
|
||||
async for chunk in response.aiter_bytes():
|
||||
file.write(chunk)
|
||||
return path
|
||||
|
||||
|
||||
def write_update_script(source: Path, target: Path) -> Path:
|
||||
updates_dir = Path(settings_payload()["app_data_dir"]) / "updates"
|
||||
updates_dir.mkdir(parents=True, exist_ok=True)
|
||||
script = updates_dir / "apply-update.ps1"
|
||||
script.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"param(",
|
||||
" [Parameter(Mandatory=$true)][int]$ProcessId,",
|
||||
" [Parameter(Mandatory=$true)][string]$Source,",
|
||||
" [Parameter(Mandatory=$true)][string]$Target",
|
||||
")",
|
||||
"$ErrorActionPreference = 'Stop'",
|
||||
"try { Wait-Process -Id $ProcessId -Timeout 60 -ErrorAction SilentlyContinue } catch {}",
|
||||
"Start-Sleep -Seconds 1",
|
||||
"Copy-Item -LiteralPath $Source -Destination $Target -Force",
|
||||
"Start-Process -FilePath $Target",
|
||||
]
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return script
|
||||
|
||||
|
||||
def exit_after_update_response() -> None:
|
||||
time.sleep(1.5)
|
||||
os._exit(0)
|
||||
|
||||
|
||||
def update_message(available: bool, latest_version: str | None, has_asset: bool) -> str:
|
||||
if not latest_version:
|
||||
return "Could not determine the latest release version."
|
||||
if not available:
|
||||
return f"TraderAI {__version__} is up to date."
|
||||
if not has_asset:
|
||||
return f"TraderAI {latest_version} is available, but the release has no {UPDATE_ASSET_NAME} asset."
|
||||
return f"TraderAI {latest_version} is available."
|
||||
|
||||
|
||||
def normalize_version(value: str) -> str | None:
|
||||
text = value.strip()
|
||||
if text.startswith("v"):
|
||||
text = text[1:]
|
||||
parts = text.split(".")
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
return text
|
||||
|
||||
|
||||
def compare_versions(left: str, right: str) -> int:
|
||||
left_parts = version_parts(left)
|
||||
right_parts = version_parts(right)
|
||||
max_len = max(len(left_parts), len(right_parts))
|
||||
left_parts.extend([0] * (max_len - len(left_parts)))
|
||||
right_parts.extend([0] * (max_len - len(right_parts)))
|
||||
return (left_parts > right_parts) - (left_parts < right_parts)
|
||||
|
||||
|
||||
def version_parts(version: str) -> list[int]:
|
||||
text = normalize_version(version) or "0"
|
||||
core = text.replace("-", ".").replace("+", ".").split(".")
|
||||
parts: list[int] = []
|
||||
for item in core:
|
||||
digits = ""
|
||||
for char in item:
|
||||
if not char.isdigit():
|
||||
break
|
||||
digits += char
|
||||
parts.append(int(digits or 0))
|
||||
return parts
|
||||
|
||||
|
||||
app = create_app()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "0.0.1"
|
||||
|
||||
RELEASES_URL = "https://git.hudsonriggs.systems/LambdaBankingConglomerate/TraderAI/releases"
|
||||
RELEASES_API_URL = "https://git.hudsonriggs.systems/api/v1/repos/LambdaBankingConglomerate/TraderAI/releases"
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,15 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
@@ -45,6 +54,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bottle"
|
||||
version = "0.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717, upload-time = "2025-06-15T10:08:59.439Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807, upload-time = "2025-06-15T10:08:57.691Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.4.22"
|
||||
@@ -54,6 +72,32 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.3"
|
||||
@@ -66,6 +110,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clr-loader"
|
||||
version = "0.2.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483, upload-time = "2026-01-03T23:13:05.439Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
@@ -182,6 +238,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
@@ -191,6 +259,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2024.8.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
@@ -200,6 +277,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proxy-tools"
|
||||
version = "0.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/cf/77d3e19b7fabd03895caca7857ef51e4c409e0ca6b37ee6e9f7daa50b642/proxy_tools-0.1.0.tar.gz", hash = "sha256:ccb3751f529c047e2d8a58440d86b205303cf0fe8146f784d1cbcd94f0a28010", size = 2978, upload-time = "2014-05-05T21:02:24.606Z" }
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.13.3"
|
||||
@@ -340,6 +432,145 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
{ name = "macholib", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pefile", marker = "sys_platform == 'win32'" },
|
||||
{ name = "pyinstaller-hooks-contrib" },
|
||||
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/60/d03d52e6690d4e9caf333dcd14550cde634ce6c118b3bc8fa3112c3186fd/pyinstaller-6.20.0.tar.gz", hash = "sha256:95c5c7e03d5d61e9dfb8ef259c699cf492bb1041beb6dbe83696608cec07347a", size = 4048728, upload-time = "2026-04-22T20:59:36.96Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/e4/e228d6d1bbb7fd62dc660a8fb202a583b023d3a3624ca95d1a9290ee4d6a/pyinstaller-6.20.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:bf3be4e1284ee78ddccba5e29f99443a12a7b4673168288ffc4c9d38c6f7b90e", size = 1047642, upload-time = "2026-04-22T20:58:32.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/bd/afb631bcb3f9040efebd4f6d067f0828b51710818f69fb41a2d4b7787f52/pyinstaller-6.20.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:72ae9c1fdea134afa791f58bdc9a1934d5c7609753c111e0026bfc272b32b712", size = 742494, upload-time = "2026-04-22T20:58:36.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/08/0729a5bac14754150e5d83b39d87d842eb42b0bffcaa03dbad6252e23a39/pyinstaller-6.20.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1031bcc307f3fbeffd4e162723e64d46dbf591c82dd0997413afb2a07328b941", size = 754191, upload-time = "2026-04-22T20:58:40.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/82/bc0ee4c7b97db1958eb651e0da9fb1e672e5ae53ca8867fd97701de52906/pyinstaller-6.20.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:8df3b3f347659fa2562d8d193a98ad4600133b8b8d07c268df89e4154376750e", size = 751902, upload-time = "2026-04-22T20:58:44.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e7/770002d6aaa54173881cb2c49bb195ba67b97bf39bac1cdf320f28401629/pyinstaller-6.20.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b0d3cc9dd8120d448459bd3880a12e2f9774c51443af49047801446377999a59", size = 748634, upload-time = "2026-04-22T20:58:48.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/db/68ba1fccb71278b2124fb90b37b7c8c0bc4c1173fba45b94466df3d9cb7f/pyinstaller-6.20.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:03696bb6350177c6bc23bcaf78e71a33c4a89b6754dd90d1be2f318e978c918b", size = 748490, upload-time = "2026-04-22T20:58:52.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0f/ac77ffa996a56be3d5c8f85734a007f8347240691657f9704e7de2527fa3/pyinstaller-6.20.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:6357f1699f6af84f37e7367f031d4f68abdba65543b83990c9e8f5a4cebed0b7", size = 747650, upload-time = "2026-04-22T20:58:57.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/56/1ee91c3a2bc10ca1f36da10a6fd55ff7efc4dec367171eb25992a827874f/pyinstaller-6.20.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0ab39c690abad26ba148e8f664f0478acc82a733997f4f22e757774832802da9", size = 747413, upload-time = "2026-04-22T20:59:01.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/55/ae264339996953c4cdf9d89d916a0a8fa26a83cf917a742fff8b9d5f3fe8/pyinstaller-6.20.0-py3-none-win32.whl", hash = "sha256:9a7637e8e44b4387b13667fdcaac86ab6b29c446c16d34d8401539b81838759c", size = 1331584, upload-time = "2026-04-22T20:59:07.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/8c/300f57578882cce259bfb5ae56fda3b69caa3fe9df40a176c719920ea6e2/pyinstaller-6.20.0-py3-none-win_amd64.whl", hash = "sha256:d588844e890ee80c4365867f98146636e1849bbca8e4284bbf0c809aff0f161a", size = 1391851, upload-time = "2026-04-22T20:59:14.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/ea/b2f8e1642aecda78c0b75c7321f708e49e10bb3c00dd4f148c40761a1527/pyinstaller-6.20.0-py3-none-win_arm64.whl", hash = "sha256:bd53282c0a73e5c95573e1ddc8e5d564d4932bec91efbaed4dc5fdff9c2ae7f2", size = 1332259, upload-time = "2026-04-22T20:59:20.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2026.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/67/f4452d68793fb15beba4f19ef39a38a8822f0da7452b503c400d5a21f5c1/pyinstaller_hooks_contrib-2026.5.tar.gz", hash = "sha256:f066dfca8f7c45ff6336c9cf9fe25b4e48bfeb322a1aa24faaedfb8a8d1b0b08", size = 173689, upload-time = "2026-05-04T22:36:55.124Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/5c/fd465d11da4d12b50d7eb5d2ee2ceb780d8d049dbb489f3828d131e387af/pyinstaller_hooks_contrib-2026.5-py3-none-any.whl", hash = "sha256:ea1535783fbdac4626351709e83f3ea80b681d3a4745763ebb407b5e27342eb9", size = 457314, upload-time = "2026-05-04T22:36:53.598Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-core"
|
||||
version = "12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-cocoa"
|
||||
version = "12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-quartz"
|
||||
version = "12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
{ name = "pyobjc-framework-cocoa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795, upload-time = "2025-11-14T09:59:46.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-security"
|
||||
version = "12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
{ name = "pyobjc-framework-cocoa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/aa/796e09a3e3d5cee32ebeebb7dcf421b48ea86e28c387924608a05e3f668b/pyobjc_framework_security-12.1.tar.gz", hash = "sha256:7fecb982bd2f7c4354513faf90ba4c53c190b7e88167984c2d0da99741de6da9", size = 168044, upload-time = "2025-11-14T10:22:06.334Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/3d/8d3a39cd292d7c76ab76233498189bc7170fc80f573b415308464f68c7ee/pyobjc_framework_security-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b2d8819f0fb7b619ec7627a0d8c1cac1a57c5143579ce8ac21548165680684b", size = 41287, upload-time = "2025-11-14T10:02:54.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/66/5160c0f938fc0515fe8d9af146aac1b093f7ef285ce797fedae161b6c0e8/pyobjc_framework_security-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab42e55f5b782332be5442750fcd9637ee33247d57c7b1d5801bc0e24ee13278", size = 41280, upload-time = "2025-11-14T10:02:58.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/48/b294ed75247c5cfa00d51925a10237337d24f54961d49a179b20a4307642/pyobjc_framework_security-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:afc36661cc6eb98cd794bed1d6668791e96557d6f72d9ac70aa49022d26af1d4", size = 41284, upload-time = "2025-11-14T10:03:01.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/57/0d3ef78779cf5c3bba878b2f824137e50978ad4a21dabe65d8b5ae0fc0d1/pyobjc_framework_security-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9510c98ab56921d1d416437372605cc1c1f6c1ad8d3061ee56b17bf423dd5427", size = 42162, upload-time = "2025-11-14T10:03:05.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/4d/63c15f9449c191e7448a05ff8af4a82c39a51bb627bc96dc9697586c0f79/pyobjc_framework_security-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6319a34508fd87ab6ca3cda6f54e707196197a65b792b292705af967e225438a", size = 41348, upload-time = "2025-11-14T10:03:08.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d8/5aaa2a8124ed04a9d6ca7053dc0fa64e42be51497ed8263a24b744a95598/pyobjc_framework_security-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:03d166371cefdef24908825148eb848f99ee2c0b865870a09dcbb94334dd3e0a", size = 42908, upload-time = "2025-11-14T10:03:13.01Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-uniformtypeidentifiers"
|
||||
version = "12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
{ name = "pyobjc-framework-cocoa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/b8/dd9d2a94509a6c16d965a7b0155e78edf520056313a80f0cd352413f0d0b/pyobjc_framework_uniformtypeidentifiers-12.1.tar.gz", hash = "sha256:64510a6df78336579e9c39b873cfcd03371c4b4be2cec8af75a8a3d07dff607d", size = 17030, upload-time = "2025-11-14T10:23:02.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/5f/1f10f5275b06d213c9897850f1fca9c881c741c1f9190cea6db982b71824/pyobjc_framework_uniformtypeidentifiers-12.1-py2.py3-none-any.whl", hash = "sha256:ec5411e39152304d2a7e0e426c3058fa37a00860af64e164794e0bcffee813f2", size = 4901, upload-time = "2025-11-14T10:05:51.532Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyobjc-framework-webkit"
|
||||
version = "12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyobjc-core" },
|
||||
{ name = "pyobjc-framework-cocoa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/14/10/110a50e8e6670765d25190ca7f7bfeecc47ec4a8c018cb928f4f82c56e04/pyobjc_framework_webkit-12.1.tar.gz", hash = "sha256:97a54dd05ab5266bd4f614e41add517ae62cdd5a30328eabb06792474b37d82a", size = 284531, upload-time = "2025-11-14T10:23:40.287Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/37/5082a0bbe12e48d4ffa53b0c0f09c77a4a6ffcfa119e26fa8dd77c08dc1c/pyobjc_framework_webkit-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3db734877025614eaef4504fadc0fbbe1279f68686a6f106f2e614e89e0d1a9d", size = 49970, upload-time = "2025-11-14T10:07:01.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/67/64920c8d201a7fc27962f467c636c4e763b43845baba2e091a50a97a5d52/pyobjc_framework_webkit-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:af2c7197447638b92aafbe4847c063b6dd5e1ed83b44d3ce7e71e4c9b042ab5a", size = 50084, upload-time = "2025-11-14T10:07:05.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/3d/80d36280164c69220ce99372f7736a028617c207e42cb587716009eecb88/pyobjc_framework_webkit-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1da0c428c9d9891c93e0de51c9f272bfeb96d34356cdf3136cb4ad56ce32ec2d", size = 50096, upload-time = "2025-11-14T10:07:10.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/7a/03c29c46866e266b0c705811c55c22625c349b0a80f5cf4776454b13dc4c/pyobjc_framework_webkit-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1a29e334d5a7dd4a4f0b5647481b6ccf8a107b92e67b2b3c6b368c899f571965", size = 50572, upload-time = "2025-11-14T10:07:14.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ac/924878f239c167ffe3bfc643aee4d6dd5b357e25f6b28db227e40e9e6df3/pyobjc_framework_webkit-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99d0d28542a266a95ee2585f51765c0331794bca461aaf4d1f5091489d475179", size = 50210, upload-time = "2025-11-14T10:07:18.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/86/637cda4983dc0936b73a385f3906256953ac434537b812814cb0b6d231a2/pyobjc_framework_webkit-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1aaa3bf12c7b68e1a36c0b294d2728e06f2cc220775e6dc4541d5046290e4dc8", size = 50680, upload-time = "2025-11-14T10:07:23.331Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
@@ -378,6 +609,49 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pythonnet"
|
||||
version = "3.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "clr-loader" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506, upload-time = "2024-12-13T08:30:40.661Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywebview"
|
||||
version = "6.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "bottle" },
|
||||
{ name = "proxy-tools" },
|
||||
{ name = "pyobjc-core", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-uniformtypeidentifiers", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pyobjc-framework-webkit", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "pythonnet", marker = "sys_platform == 'win32'" },
|
||||
{ name = "qtpy", marker = "sys_platform == 'openbsd6'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/59/4a/05307135dafba67778669d194bd1a3822a7685ec9ee8a6d7e70856c1a551/pywebview-6.2.1.tar.gz", hash = "sha256:71b7136752e40824655304d938efb62014218d1a90bd8e87e1cbdb1ce9c466af", size = 513126, upload-time = "2026-04-15T09:02:16.595Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/25/9491695c22c4842c5b3903b4dc172e0eecf67a27c0af34a71512c9b76a0a/pywebview-6.2.1-py3-none-any.whl", hash = "sha256:9d07275f53894ab4d5e2e0e996227193e7187dec276d9b624dccbce029216b46", size = 525463, upload-time = "2026-04-15T09:02:10.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
@@ -433,6 +707,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qtpy"
|
||||
version = "2.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "respx"
|
||||
version = "0.23.1"
|
||||
@@ -445,6 +731,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/4a/221da6ca167db45693d8d26c7dc79ccfc978a440251bf6721c9aaf251ac0/respx-0.23.1-py2.py3-none-any.whl", hash = "sha256:b18004b029935384bccfa6d7d9d74b4ec9af73a081cc28600fffc0447f4b8c1a", size = 25557, upload-time = "2026-04-08T14:37:14.613Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "82.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "1.0.0"
|
||||
@@ -460,7 +755,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "traderai"
|
||||
version = "0.1.0"
|
||||
version = "0.0.1"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "apscheduler" },
|
||||
@@ -469,12 +764,14 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pywebview" },
|
||||
{ name = "tzlocal" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pyinstaller" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "respx" },
|
||||
@@ -487,9 +784,11 @@ requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.27.0" },
|
||||
{ name = "pydantic", specifier = ">=2.8.0" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.4.0" },
|
||||
{ name = "pyinstaller", marker = "extra == 'dev'", specifier = ">=6.11.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.8" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
||||
{ name = "pywebview", specifier = ">=5.4" },
|
||||
{ name = "respx", marker = "extra == 'dev'", specifier = ">=0.21.1" },
|
||||
{ name = "tzlocal", specifier = ">=5.2" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30.0" },
|
||||
@@ -745,3 +1044,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
+295
-2
@@ -7,8 +7,35 @@ const warningEl = document.getElementById("warning");
|
||||
const memoryInspectorEl = document.getElementById("memory-inspector");
|
||||
const memoryRefreshButton = document.getElementById("memory-refresh");
|
||||
const memoryClearButton = document.getElementById("memory-clear");
|
||||
const configForm = document.getElementById("config-form");
|
||||
const configRefreshButton = document.getElementById("config-refresh");
|
||||
const configStatusEl = document.getElementById("config-status");
|
||||
const configPathsEl = document.getElementById("config-paths");
|
||||
const settingsToggle = document.getElementById("settings-toggle");
|
||||
const memoryToggle = document.getElementById("memory-toggle");
|
||||
const ollamaToggle = document.getElementById("ollama-toggle");
|
||||
const settingsPanel = document.getElementById("settings-panel");
|
||||
const memoryPanel = document.getElementById("memory-panel");
|
||||
const ollamaPanel = document.getElementById("ollama-panel");
|
||||
const ollamaForm = document.getElementById("ollama-config-form");
|
||||
const ollamaRefreshButton = document.getElementById("ollama-refresh");
|
||||
const ollamaDownloadButton = document.getElementById("ollama-download");
|
||||
const ollamaInstallButton = document.getElementById("ollama-install");
|
||||
const ollamaLaunchButton = document.getElementById("ollama-launch");
|
||||
const ollamaPullButton = document.getElementById("ollama-pull");
|
||||
const ollamaStatusEl = document.getElementById("ollama-status");
|
||||
const ollamaMessageEl = document.getElementById("ollama-message");
|
||||
const updateCheckButton = document.getElementById("update-check");
|
||||
const updateInstallButton = document.getElementById("update-install");
|
||||
const updateOpenReleasesButton = document.getElementById("update-open-releases");
|
||||
const updateStatusEl = document.getElementById("update-status");
|
||||
|
||||
let ollamaOnline = true;
|
||||
let latestUpdate = null;
|
||||
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
|
||||
function addMessage(role, text) {
|
||||
const node = document.createElement("div");
|
||||
@@ -381,6 +408,255 @@ function setWarning(text) {
|
||||
warningEl.textContent = text || "";
|
||||
}
|
||||
|
||||
function fetchErrorMessage(error) {
|
||||
if (error instanceof TypeError && /fetch/i.test(error.message)) {
|
||||
return "TraderAI backend is not reachable. Close this app window and launch TraderAI.exe again.";
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
|
||||
const configFieldIds = {
|
||||
ollama_base_url: "config-ollama-base-url",
|
||||
ollama_model: "config-ollama-model",
|
||||
ollama_num_ctx: "config-ollama-num-ctx",
|
||||
uex_base_url: "config-uex-base-url",
|
||||
uex_secret_key: "config-uex-secret-key",
|
||||
uex_bearer_token: "config-uex-bearer-token",
|
||||
traderai_user_name: "config-traderai-user-name",
|
||||
traderai_memory_path: "config-traderai-memory-path",
|
||||
uex_notification_poll_seconds: "config-uex-notification-poll-seconds",
|
||||
require_write_approval: "config-require-write-approval",
|
||||
};
|
||||
|
||||
const ollamaFieldIds = {
|
||||
ollama_base_url: "ollama-base-url",
|
||||
ollama_model: "ollama-model",
|
||||
ollama_num_ctx: "ollama-num-ctx",
|
||||
};
|
||||
|
||||
async function refreshConfig() {
|
||||
try {
|
||||
const response = await fetch("/api/config");
|
||||
const config = await response.json();
|
||||
renderConfig(config);
|
||||
} catch (error) {
|
||||
configStatusEl.textContent = `Config load failed: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderConfig(config) {
|
||||
const values = config.values || {};
|
||||
for (const [key, id] of Object.entries(configFieldIds)) {
|
||||
const field = document.getElementById(id);
|
||||
if (!field) continue;
|
||||
if (field.type === "checkbox") {
|
||||
field.checked = Boolean(values[key]);
|
||||
} else {
|
||||
field.value = values[key] ?? "";
|
||||
}
|
||||
}
|
||||
for (const [key, id] of Object.entries(ollamaFieldIds)) {
|
||||
const field = document.getElementById(id);
|
||||
if (!field) continue;
|
||||
field.value = values[key] ?? "";
|
||||
}
|
||||
configPathsEl.textContent = `App data: ${config.app_data_dir}\nConfig: ${config.config_path}\nLog: ${config.log_path}\nEdge profile: ${config.edge_profile_dir}`;
|
||||
configStatusEl.textContent = "";
|
||||
}
|
||||
|
||||
async function saveConfig(event) {
|
||||
event.preventDefault();
|
||||
const values = {};
|
||||
for (const [key, id] of Object.entries(configFieldIds)) {
|
||||
const field = document.getElementById(id);
|
||||
if (!field) continue;
|
||||
values[key] = field.type === "checkbox" ? field.checked : field.value;
|
||||
}
|
||||
configStatusEl.textContent = "Saving";
|
||||
try {
|
||||
const response = await fetch("/api/config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ values }),
|
||||
});
|
||||
const result = await response.json();
|
||||
renderConfig(result);
|
||||
configStatusEl.textContent = result.message || "Saved";
|
||||
addMessage("assistant", "Config saved. Restart TraderAI for the new settings to fully apply.");
|
||||
} catch (error) {
|
||||
configStatusEl.textContent = `Config save failed: ${fetchErrorMessage(error)}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOllamaConfig(event) {
|
||||
event.preventDefault();
|
||||
const values = {};
|
||||
for (const [key, id] of Object.entries(ollamaFieldIds)) {
|
||||
const field = document.getElementById(id);
|
||||
if (!field) continue;
|
||||
values[key] = field.value;
|
||||
}
|
||||
setOllamaMessage("Saving Ollama config");
|
||||
try {
|
||||
const response = await fetch("/api/config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ values }),
|
||||
});
|
||||
const result = await response.json();
|
||||
renderConfig(result);
|
||||
setOllamaMessage(result.message || "Saved");
|
||||
await refreshOllamaStatus();
|
||||
} catch (error) {
|
||||
setOllamaMessage(`Ollama config save failed: ${fetchErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshOllamaStatus() {
|
||||
if (!ollamaStatusEl) return;
|
||||
ollamaStatusEl.textContent = "Checking Ollama";
|
||||
try {
|
||||
const response = await fetch("/api/ollama/status");
|
||||
const status = await response.json();
|
||||
renderOllamaStatus(status);
|
||||
} catch (error) {
|
||||
ollamaStatusEl.textContent = `Ollama status failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderOllamaStatus(status) {
|
||||
if (!ollamaStatusEl) return;
|
||||
const models = status.models?.length ? status.models.join(", ") : "None detected";
|
||||
const pillClass = status.installed && status.running && status.model_available ? "status-pill" : "status-pill warning";
|
||||
ollamaStatusEl.innerHTML = `
|
||||
<div class="${pillClass}">${escapeHtml(status.message || "Unknown")}</div>
|
||||
<div class="ollama-status-grid">
|
||||
${ollamaStatusItem("Installed", status.installed ? "Yes" : "No")}
|
||||
${ollamaStatusItem("Running", status.running ? "Yes" : "No")}
|
||||
${ollamaStatusItem("Model", status.configured_model || "")}
|
||||
${ollamaStatusItem("Pulled", status.model_available ? "Yes" : "No")}
|
||||
${ollamaStatusItem("URL", status.base_url || "")}
|
||||
${ollamaStatusItem("Auto Install", status.can_auto_install ? "Available" : "Unavailable")}
|
||||
</div>
|
||||
${ollamaStatusItem("Installed Models", models)}
|
||||
${status.detail ? ollamaStatusItem("Detail", status.detail) : ""}
|
||||
`;
|
||||
if (ollamaInstallButton) ollamaInstallButton.disabled = Boolean(status.installed);
|
||||
if (ollamaLaunchButton) ollamaLaunchButton.disabled = !status.installed || Boolean(status.running);
|
||||
if (ollamaPullButton) ollamaPullButton.disabled = !status.running || Boolean(status.model_available);
|
||||
}
|
||||
|
||||
function ollamaStatusItem(label, value) {
|
||||
return `<div class="ollama-status-item"><strong>${escapeHtml(label)}</strong><span>${escapeHtml(String(value ?? ""))}</span></div>`;
|
||||
}
|
||||
|
||||
function setOllamaMessage(message) {
|
||||
if (ollamaMessageEl) ollamaMessageEl.textContent = message || "";
|
||||
}
|
||||
|
||||
async function postOllamaAction(endpoint, options = {}) {
|
||||
setOllamaMessage("Working");
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: options.body ? { "Content-Type": "application/json" } : undefined,
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.detail || `HTTP ${response.status}`);
|
||||
setOllamaMessage(result.message || "Done");
|
||||
await refreshOllamaStatus();
|
||||
} catch (error) {
|
||||
setOllamaMessage(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function configuredOllamaModel() {
|
||||
return document.getElementById("ollama-model")?.value || document.getElementById("config-ollama-model")?.value || "";
|
||||
}
|
||||
|
||||
async function checkForUpdate() {
|
||||
if (!updateStatusEl) return;
|
||||
updateStatusEl.textContent = "Checking releases";
|
||||
try {
|
||||
const response = await fetch("/api/update/check");
|
||||
const result = await response.json();
|
||||
latestUpdate = result;
|
||||
renderUpdateStatus(result);
|
||||
} catch (error) {
|
||||
updateStatusEl.textContent = `Update check failed: ${error.message}`;
|
||||
if (updateInstallButton) updateInstallButton.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function renderUpdateStatus(update) {
|
||||
if (!updateStatusEl) return;
|
||||
const lines = [
|
||||
`Current: ${update.current_version || "unknown"}`,
|
||||
`Latest: ${update.latest_version || "unknown"}`,
|
||||
update.message || "",
|
||||
].filter(Boolean);
|
||||
if (update.available && !update.asset_download_url) {
|
||||
lines.push("The release needs a TraderAI.exe attachment before the app can self-update.");
|
||||
}
|
||||
if (update.available && update.asset_download_url && !update.packaged) {
|
||||
lines.push("Self-update runs from the packaged desktop exe.");
|
||||
}
|
||||
updateStatusEl.textContent = lines.join("\n");
|
||||
if (updateInstallButton) {
|
||||
updateInstallButton.disabled = !update.available || !update.asset_download_url || !update.packaged;
|
||||
}
|
||||
}
|
||||
|
||||
async function installUpdate() {
|
||||
if (!updateStatusEl) return;
|
||||
updateStatusEl.textContent = "Downloading update";
|
||||
try {
|
||||
const response = await fetch("/api/update/install", { method: "POST" });
|
||||
const result = await response.json();
|
||||
latestUpdate = result;
|
||||
renderUpdateStatus(result);
|
||||
} catch (error) {
|
||||
updateStatusEl.textContent = `Update failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function openReleasesPage() {
|
||||
const url = latestUpdate?.release_url || "https://git.hudsonriggs.systems/LambdaBankingConglomerate/TraderAI/releases";
|
||||
window.open(url, "_blank", "noreferrer");
|
||||
}
|
||||
|
||||
function toggleSidebarPanel(panelName) {
|
||||
const panels = {
|
||||
settings: { panel: settingsPanel, button: settingsToggle },
|
||||
memory: { panel: memoryPanel, button: memoryToggle },
|
||||
ollama: { panel: ollamaPanel, button: ollamaToggle },
|
||||
};
|
||||
const target = panels[panelName];
|
||||
if (!target?.panel || !target?.button) return;
|
||||
const shouldOpen = target.panel.hidden;
|
||||
for (const item of Object.values(panels)) {
|
||||
if (!item.panel || !item.button) continue;
|
||||
item.panel.hidden = true;
|
||||
item.button.classList.remove("active");
|
||||
item.button.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
if (shouldOpen) {
|
||||
target.panel.hidden = false;
|
||||
target.button.classList.add("active");
|
||||
target.button.setAttribute("aria-expanded", "true");
|
||||
if (panelName === "settings") {
|
||||
refreshConfig();
|
||||
checkForUpdate();
|
||||
}
|
||||
if (panelName === "memory") refreshMemory();
|
||||
if (panelName === "ollama") {
|
||||
refreshConfig();
|
||||
refreshOllamaStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const response = await fetch("/api/health");
|
||||
@@ -576,8 +852,22 @@ input.addEventListener("keydown", async (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
memoryRefreshButton.addEventListener("click", refreshMemory);
|
||||
memoryClearButton.addEventListener("click", clearMemory);
|
||||
memoryRefreshButton?.addEventListener("click", refreshMemory);
|
||||
memoryClearButton?.addEventListener("click", clearMemory);
|
||||
configRefreshButton?.addEventListener("click", refreshConfig);
|
||||
configForm?.addEventListener("submit", saveConfig);
|
||||
settingsToggle?.addEventListener("click", () => toggleSidebarPanel("settings"));
|
||||
memoryToggle?.addEventListener("click", () => toggleSidebarPanel("memory"));
|
||||
ollamaToggle?.addEventListener("click", () => toggleSidebarPanel("ollama"));
|
||||
ollamaForm?.addEventListener("submit", saveOllamaConfig);
|
||||
ollamaRefreshButton?.addEventListener("click", refreshOllamaStatus);
|
||||
ollamaDownloadButton?.addEventListener("click", () => postOllamaAction("/api/ollama/download"));
|
||||
ollamaInstallButton?.addEventListener("click", () => postOllamaAction("/api/ollama/install"));
|
||||
ollamaLaunchButton?.addEventListener("click", () => postOllamaAction("/api/ollama/launch"));
|
||||
ollamaPullButton?.addEventListener("click", () => postOllamaAction("/api/ollama/pull", { body: { model: configuredOllamaModel() } }));
|
||||
updateCheckButton?.addEventListener("click", checkForUpdate);
|
||||
updateInstallButton?.addEventListener("click", installUpdate);
|
||||
updateOpenReleasesButton?.addEventListener("click", openReleasesPage);
|
||||
|
||||
async function sendMessage() {
|
||||
const message = input.value.trim();
|
||||
@@ -663,6 +953,9 @@ async function sendMessage() {
|
||||
addMessage("assistant", "Tell me what to find or draft on UEX. I will ask for approval before sending anything.");
|
||||
refreshPending();
|
||||
refreshMemory();
|
||||
refreshConfig();
|
||||
refreshOllamaStatus();
|
||||
checkForUpdate();
|
||||
pollNotifications();
|
||||
checkHealth();
|
||||
setInterval(checkHealth, 30000);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill-rule="evenodd" clip-rule="evenodd" d="M168.64 23.253c4.608 1.814 8.768 4.8 12.544 8.747 6.293 6.528 11.605 15.872 15.659 26.944 4.074 11.136 6.72 23.467 7.722 35.84a107.824 107.824 0 0143.712-13.568l1.088-.085c18.56-1.494 36.907 1.856 52.907 10.112a103.091 103.091 0 016.336 3.626c1.067-12.138 3.669-24.192 7.68-35.072 4.053-11.093 9.365-20.416 15.637-26.965a35.628 35.628 0 0112.566-8.747c5.482-2.133 11.306-2.517 16.981-.896 8.555 2.432 15.893 7.851 21.675 15.723 5.29 7.19 9.258 16.405 11.968 27.456 4.906 19.925 5.76 46.144 2.453 77.76l1.131.853.554.406c16.15 12.288 27.392 29.802 33.344 50.133 9.28 31.723 4.608 67.307-11.392 87.211l-.384.448.043.064c8.896 16.256 14.293 33.429 15.445 51.2l.043.64c1.365 22.72-4.267 45.589-17.365 68.053l-.15.213.214.512c10.069 24.683 13.226 49.536 9.344 74.368l-.128.832a13.888 13.888 0 01-15.936 11.435 13.83 13.83 0 01-11.31-10.43 13.828 13.828 0 01-.21-5.399c3.562-22.038.213-44.139-10.24-66.624a13.713 13.713 0 01.853-13.163l.085-.128c12.886-19.712 18.219-39.04 17.067-58.027-.981-16.618-6.933-32.938-17.067-48.49a13.737 13.737 0 013.84-18.902l.192-.128c5.184-3.392 9.963-12.053 12.374-23.893a90.218 90.218 0 00-2.027-42.112c-4.373-14.933-12.373-27.392-23.573-35.904-12.694-9.685-29.504-14.357-50.774-13.013a13.93 13.93 0 01-13.482-7.915c-6.699-14.187-16.47-24.341-28.651-30.635a70.145 70.145 0 00-37.803-7.082c-26.56 2.112-49.984 17.088-56.96 35.968a13.91 13.91 0 01-13.013 9.066c-22.763.043-40.384 5.376-53.269 14.998-11.136 8.32-18.731 19.946-22.742 33.877a86.824 86.824 0 00-1.45 40.235c2.389 11.904 7.061 21.76 12.416 27.072l.17.149c4.523 4.416 5.483 11.307 2.326 16.747-7.68 13.269-13.419 33.045-14.358 52.053-1.066 21.717 3.968 40.576 15.339 54.101l.341.406a13.711 13.711 0 012.027 14.72c-12.288 26.368-16.064 48.042-11.989 65.109a13.91 13.91 0 01-27.072 6.357c-5.184-21.717-1.664-46.592 10.09-74.624l.299-.746-.17-.256a92.574 92.574 0 01-12.758-27.926l-.107-.405a122.965 122.965 0 01-3.776-38.08c.939-19.413 5.931-39.296 13.27-55.253l.256-.555-.043-.043c-6.25-8.917-10.88-20.33-13.44-32.96l-.107-.512a114.176 114.176 0 011.984-53.12c5.59-19.52 16.576-36.288 32.768-48.405 1.28-.96 2.624-1.92 3.968-2.816-3.392-31.851-2.538-58.24 2.39-78.293 2.709-11.051 6.698-20.267 11.989-27.456 5.76-7.851 13.099-13.27 21.653-15.723 5.675-1.621 11.52-1.259 17.003.896v.021zm87.808 193.92c19.968 0 38.4 6.678 52.181 18.24 13.44 11.243 21.44 26.347 21.44 41.387 0 18.944-8.661 33.707-24.17 43.136-13.227 8-30.955 11.883-51.264 11.883-21.526 0-39.915-5.526-53.184-15.659-13.163-10.027-20.544-24.107-20.544-39.36 0-15.083 8.49-30.229 22.528-41.515 14.25-11.456 33.066-18.112 53.013-18.112zm0 19.115a65.498 65.498 0 00-40.875 13.867c-9.834 7.893-15.402 17.813-15.402 26.666 0 9.131 4.48 17.686 13.013 24.192 9.707 7.403 23.979 11.691 41.451 11.691 17.045 0 31.424-3.136 41.216-9.088 9.877-5.973 14.933-14.635 14.933-26.816 0-9.024-5.248-18.987-14.571-26.795-10.325-8.64-24.32-13.717-39.765-13.717zm14.123 25.813l.085.086a7.431 7.431 0 01-1.195 10.453l-6.229 4.907v9.514a7.999 7.999 0 01-8.021 7.958 8.004 8.004 0 01-8.022-7.958v-9.813l-5.781-4.651a7.4 7.4 0 01-1.109-10.453 7.53 7.53 0 0110.538-1.088l4.587 3.669 4.693-3.712a7.533 7.533 0 0110.454 1.088zm-107.52-40.938c10.197 0 18.496 8.32 18.496 18.581a18.564 18.564 0 01-18.518 18.581 18.559 18.559 0 01-18.496-18.56 18.565 18.565 0 015.399-13.129 18.609 18.609 0 0113.119-5.473zm185.728 0c10.24 0 18.517 8.32 18.517 18.581a18.559 18.559 0 01-18.517 18.581 18.56 18.56 0 01-18.496-18.56 18.56 18.56 0 0118.496-18.602zM158.72 49.067l-.064.042a14.06 14.06 0 00-6.08 5.078l-.107.128c-2.944 4.032-5.504 9.962-7.424 17.749-3.626 14.763-4.608 34.795-2.645 59.349 9.173-2.73 19.179-4.437 29.952-5.056l.213-.021.406-.725a69.41 69.41 0 013.157-5.099c2.624-16.448.469-36.096-5.397-52.139-2.859-7.765-6.336-13.866-9.664-17.344a13.403 13.403 0 00-2.283-1.92l-.064-.042zm195.712.853l-.043.021a13.396 13.396 0 00-2.282 1.92c-3.328 3.478-6.827 9.6-9.664 17.366-6.187 16.938-8.256 37.888-4.907 54.869l1.237 2.069.171.299h.64a110.599 110.599 0 0131.275 4.523c1.834-23.979.81-43.584-2.731-58.07-1.92-7.786-4.48-13.717-7.445-17.749l-.086-.128a14.054 14.054 0 00-6.08-5.099h-.085v-.021z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
+83
-12
@@ -4,6 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>TraderAI</title>
|
||||
<link rel="icon" href="/static/art/LBC_Logo.ico" sizes="any">
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -36,23 +37,93 @@
|
||||
<h2>Pending Approval</h2>
|
||||
<div id="pending-actions" class="pending-empty">No pending actions</div>
|
||||
</section>
|
||||
<section class="side-section">
|
||||
<div class="section-title-row">
|
||||
<h2>Memory</h2>
|
||||
<button class="secondary small-button" id="memory-refresh" type="button">Refresh</button>
|
||||
<section class="side-section sidebar-tools">
|
||||
<div class="sidebar-tool-buttons" role="tablist" aria-label="Sidebar panels">
|
||||
<button class="sidebar-tool-button" id="settings-toggle" type="button" aria-expanded="false" aria-controls="settings-panel" title="Settings">
|
||||
<i data-lucide="settings" aria-hidden="true"></i>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<button class="sidebar-tool-button" id="memory-toggle" type="button" aria-expanded="false" aria-controls="memory-panel" title="Memory">
|
||||
<i data-lucide="brain" aria-hidden="true"></i>
|
||||
<span>Memory</span>
|
||||
</button>
|
||||
<button class="sidebar-tool-button" id="ollama-toggle" type="button" aria-expanded="false" aria-controls="ollama-panel" title="Ollama">
|
||||
<img class="sidebar-tool-image" src="/static/art/ollama-icon.svg" alt="" onerror="this.remove();">
|
||||
<i data-lucide="bot" aria-hidden="true"></i>
|
||||
<span>Ollama</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="memory-controls">
|
||||
<label><input type="checkbox" id="clear-memories" checked> Memories</label>
|
||||
<label><input type="checkbox" id="clear-conversations" checked> Chat</label>
|
||||
<label><input type="checkbox" id="clear-outbox" checked> Notices</label>
|
||||
<label><input type="checkbox" id="clear-profile"> Profile</label>
|
||||
<label><input type="checkbox" id="clear-jobs"> Jobs</label>
|
||||
<div class="sidebar-panel" id="settings-panel" hidden>
|
||||
<div class="section-title-row">
|
||||
<h2>Config</h2>
|
||||
<button class="secondary small-button" id="config-refresh" type="button">Refresh</button>
|
||||
</div>
|
||||
<form class="config-form" id="config-form">
|
||||
<label>Ollama URL<input id="config-ollama-base-url" name="ollama_base_url" type="text"></label>
|
||||
<label>Ollama Model<input id="config-ollama-model" name="ollama_model" type="text"></label>
|
||||
<label>Context Tokens<input id="config-ollama-num-ctx" name="ollama_num_ctx" type="number" min="1024" step="1024"></label>
|
||||
<label>UEX API URL<input id="config-uex-base-url" name="uex_base_url" type="text"></label>
|
||||
<label>UEX Secret Key<input id="config-uex-secret-key" name="uex_secret_key" type="password" autocomplete="off"></label>
|
||||
<label>UEX Bearer Token<input id="config-uex-bearer-token" name="uex_bearer_token" type="password" autocomplete="off"></label>
|
||||
<label>UEX Username<input id="config-traderai-user-name" name="traderai_user_name" type="text"></label>
|
||||
<label>Memory DB Path<input id="config-traderai-memory-path" name="traderai_memory_path" type="text"></label>
|
||||
<label>Notification Poll Seconds<input id="config-uex-notification-poll-seconds" name="uex_notification_poll_seconds" type="number" min="15" step="15"></label>
|
||||
<label class="config-check"><input id="config-require-write-approval" name="require_write_approval" type="checkbox"> Require write approval</label>
|
||||
<div class="config-paths" id="config-paths"></div>
|
||||
<button type="submit">Save Config</button>
|
||||
<div class="config-status" id="config-status"></div>
|
||||
</form>
|
||||
<div class="update-box">
|
||||
<div class="section-title-row">
|
||||
<h2>Updates</h2>
|
||||
<button class="secondary small-button" id="update-check" type="button">Check</button>
|
||||
</div>
|
||||
<div class="update-status" id="update-status"></div>
|
||||
<div class="update-actions">
|
||||
<button class="secondary small-button" id="update-open-releases" type="button">Releases</button>
|
||||
<button class="small-button" id="update-install" type="button">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-panel" id="memory-panel" hidden>
|
||||
<div class="section-title-row">
|
||||
<h2>Memory</h2>
|
||||
<button class="secondary small-button" id="memory-refresh" type="button">Refresh</button>
|
||||
</div>
|
||||
<div class="memory-controls">
|
||||
<label><input type="checkbox" id="clear-memories" checked> Memories</label>
|
||||
<label><input type="checkbox" id="clear-conversations" checked> Chat</label>
|
||||
<label><input type="checkbox" id="clear-outbox" checked> Notices</label>
|
||||
<label><input type="checkbox" id="clear-profile"> Profile</label>
|
||||
<label><input type="checkbox" id="clear-jobs"> Jobs</label>
|
||||
</div>
|
||||
<button class="danger-button" id="memory-clear" type="button">Clear Selected</button>
|
||||
<div id="memory-inspector" class="memory-inspector"></div>
|
||||
</div>
|
||||
<div class="sidebar-panel" id="ollama-panel" hidden>
|
||||
<div class="section-title-row">
|
||||
<h2>Ollama</h2>
|
||||
<button class="secondary small-button" id="ollama-refresh" type="button">Refresh</button>
|
||||
</div>
|
||||
<form class="config-form" id="ollama-config-form">
|
||||
<label>Ollama URL<input id="ollama-base-url" name="ollama_base_url" type="text"></label>
|
||||
<label>Model<input id="ollama-model" name="ollama_model" type="text"></label>
|
||||
<label>Context Tokens<input id="ollama-num-ctx" name="ollama_num_ctx" type="number" min="1024" step="1024"></label>
|
||||
<button type="submit">Save Ollama Config</button>
|
||||
</form>
|
||||
<div class="ollama-status" id="ollama-status"></div>
|
||||
<div class="ollama-actions">
|
||||
<button class="secondary small-button" id="ollama-download" type="button">Download</button>
|
||||
<button class="secondary small-button" id="ollama-install" type="button">Auto Install</button>
|
||||
<button class="secondary small-button" id="ollama-launch" type="button">Launch</button>
|
||||
<button class="small-button" id="ollama-pull" type="button">Install Model</button>
|
||||
</div>
|
||||
<div class="config-status" id="ollama-message"></div>
|
||||
</div>
|
||||
<button class="danger-button" id="memory-clear" type="button">Clear Selected</button>
|
||||
<div id="memory-inspector" class="memory-inspector"></div>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
<script src="https://unpkg.com/lucide@0.562.0/dist/umd/lucide.min.js"></script>
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+222
-1
@@ -205,7 +205,7 @@ h2 {
|
||||
.messages {
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
padding: 28px;
|
||||
padding: 28px 28px 18px;
|
||||
background:
|
||||
radial-gradient(circle at 50% 12%, rgba(212, 175, 55, 0.1), transparent 34%),
|
||||
linear-gradient(180deg, rgba(247, 241, 220, 0.58), rgba(255, 250, 240, 0.64));
|
||||
@@ -360,8 +360,12 @@ h2 {
|
||||
}
|
||||
|
||||
.composer-wrap {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 5;
|
||||
border-top: 1px solid var(--line);
|
||||
background: rgba(255, 250, 240, 0.88);
|
||||
box-shadow: 0 -16px 34px rgba(38, 58, 27, 0.12);
|
||||
}
|
||||
|
||||
.message-activity {
|
||||
@@ -543,6 +547,29 @@ textarea:disabled {
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 10px;
|
||||
outline: none;
|
||||
background: #fffdf7;
|
||||
color: var(--brown);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="number"]:focus {
|
||||
border-color: var(--gold);
|
||||
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.18);
|
||||
}
|
||||
|
||||
button {
|
||||
min-width: 104px;
|
||||
border: 0;
|
||||
@@ -605,6 +632,200 @@ button.secondary {
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.sidebar-tools {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.sidebar-tool-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sidebar-tool-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 46px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 14px;
|
||||
background: #fff9e9;
|
||||
color: var(--forest);
|
||||
font-family: Inter, "Segoe UI", Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 10px 22px rgba(38, 58, 27, 0.08);
|
||||
}
|
||||
|
||||
.sidebar-tool-button svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke-width: 2.3;
|
||||
}
|
||||
|
||||
.sidebar-tool-image {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.sidebar-tool-image + svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-tool-button.active {
|
||||
border-color: rgba(212, 175, 55, 0.72);
|
||||
background: linear-gradient(180deg, #345326, #1f3416);
|
||||
color: var(--ivory);
|
||||
}
|
||||
|
||||
.sidebar-panel {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.config-form {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.config-form label {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.config-check {
|
||||
display: flex !important;
|
||||
grid-template-columns: none;
|
||||
align-items: center;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.config-form button {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.config-paths,
|
||||
.config-status {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.config-status {
|
||||
color: var(--forest);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ollama-status {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 14px 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 250, 240, 0.78);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ollama-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ollama-status-item {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ollama-status-item strong {
|
||||
color: var(--forest);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ollama-status-item span {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
min-height: 24px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid rgba(52, 83, 38, 0.28);
|
||||
border-radius: 999px;
|
||||
background: #edf3df;
|
||||
color: var(--forest);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-pill.warning {
|
||||
border-color: rgba(159, 60, 50, 0.26);
|
||||
background: #fff2e5;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.ollama-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ollama-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.update-box {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.update-box h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.update-status {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 250, 240, 0.78);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.update-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.update-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user