Compare commits
24 Commits
26ff4d3ef2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 096b9868a7 | |||
| ea460864a6 | |||
| 58359294bc | |||
| c7f949f69a | |||
| e0627f5809 | |||
| 92641bf863 | |||
| 3b674f9cf6 | |||
| 66e7d6e686 | |||
| 0957027eed | |||
| c0e0325958 | |||
| f5219bf2e4 | |||
| 220d1b67b5 | |||
| c1826ebab6 | |||
|
|
48eeda1a2d | ||
|
|
cb6c4a2b1b | ||
|
|
c707fb59bd | ||
| 1907996291 | |||
| dbd1ea1ebf | |||
| 0782fb29d0 | |||
|
|
fc23473569 | ||
|
|
79407eeb5c | ||
| 1162815134 | |||
| 3cd9ce1cf8 | |||
| a186847e9f |
5
.bsd3-license-header.tmpl
Normal file
5
.bsd3-license-header.tmpl
Normal file
@@ -0,0 +1,5 @@
|
||||
Copyright (c) $years, $owner
|
||||
All rights reserved.
|
||||
|
||||
This source code is licensed under the BSD-3-Clause license found in the
|
||||
LICENSE file in the root directory of this source tree.
|
||||
8
.env.example
Normal file
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
||||
# LLM API Configuration (OpenAI-compatible endpoint)
|
||||
LLM_API_KEY=your_llm_api_key_here
|
||||
LLM_API_BASE=http://localhost:11434/v1
|
||||
|
||||
# Qdrant Vector Database Configuration
|
||||
QDRANT_HOST=localhost
|
||||
QDRANT_PORT=6333
|
||||
QDRANT_API_KEY=your_qdrant_api_key_here
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -168,3 +168,4 @@ cython_debug/
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
app_database/
|
||||
562
README.md
562
README.md
@@ -1,309 +1,355 @@
|
||||
# Gamayun
|
||||
# semeion
|
||||
|
||||

|
||||
|
||||
## Concept
|
||||
## Overview
|
||||
|
||||
Desktop application for forensic investigators which allows to search for various types of artifacts via natural language.
|
||||
The application uses a multi-stage hybrid search approach which accepts a query in natural language and then utilizes a GPT to generate a machine-readable search plan which can be adjusted by the user before execution.
|
||||
The system then utilizes a combination of semantic search via pre-generated embeddings and filters the results as defined via the machine interface.
|
||||
This enables the combination of semantic understanding with context and temporal relationships rather than traditional approaches of exact keyword matching.
|
||||
Semeion is a **timeline-first digital forensics analysis platform** that solves the two biggest pain points in modern investigations: navigating massive communication datasets and correlating events across data sources, including filesystm artifacts and OS events.
|
||||
|
||||
## UX Example
|
||||
> **Core Question Semeion Answers:** "What happened around this time—and how are these events connected?"
|
||||
|
||||
An investigator can ask "show me what happened after they discussed the payment" and the system will find relevant communication about payments, then correlate subsequent activities (file access, application launches, network traffic) in a temporal sequence, regardless of the specific applications or messaging platforms involved.
|
||||
Traditional forensic tools often have unintuitiveur slow user experience (opinionated). Semeion in contrast aims to provide a fast, intelligent timeline that automatically clusters semantically-related events, enabling investigators to see patterns at a glance rather than clicking through endless individual entries.
|
||||
|
||||
## System Overview
|
||||
An investigator opens Semeion alongside their primary forensic tool, either searches for relevant content (like "*discussions about data transfer*" in Russian messages) or navigates to a known timestamp (from incident logs), and immediately sees all correlated activity—messages, file access, network connections, system events—organized into meaningful sequences rather than flat lists.
|
||||
|
||||
### Core Concept
|
||||
## Key Features
|
||||
|
||||
The system treats all digital artifacts as semantically-rich objects embedded in a multi-dimensional vector space. Instead of matching exact words, it finds artifacts with similar meaning. A local language model interprets natural language queries and decomposes them into structured search operations that can handle temporal reasoning ("after"), relationship detection ("both in"), and causal inference ("because of").
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Timeline Visualization** | Render millions of events with <200ms response time; smooth zooming/panning |
|
||||
| **Semantic Event Clustering** | Automatically group related events into patterns (file transfers, communication bursts, login sequences) |
|
||||
| **Multilingual Semantic Search** | Find concepts across languages—search in English, find results in Russian/Chinese/Arabic with translation |
|
||||
| **Two-Mode Operation** | Semantic content discovery → timeline analysis OR timestamp → content correlation |
|
||||
| **Cross-Source Correlation** | Unified timeline across messages, files, network, browser, system events |
|
||||
| **Event Grouping** | Collapse/expand semantically-related events; discover patterns |
|
||||
| **Native Desktop Application** | PySide6-based—runs fully local, tailored for forensic-grade airgapped environments |
|
||||
| **Open Source & Transparent** | Auditable methods, minimize "blackbox" AI, court-defensible results |
|
||||
|
||||
### Key Innovation
|
||||
## The Problem Semeion Solves
|
||||
|
||||
Generic artifact understanding: The system doesn't need to "know" about TOX, WhatsApp, Signal, Telegram, or any specific application. During ingestion, it automatically classifies content as "chat message," "system event," "document," etc., based purely on semantic similarity to type descriptions. This means it works on artifacts from applications that don't even exist yet, or proprietary communication tools without public documentation.
|
||||
### Problematic UX with existing forensic suites
|
||||
|
||||
### Architecture Philosophy
|
||||
Existing forensic tools have timeline interfaces that:
|
||||
|
||||
Client-Server Separation: Compute-intensive operations (embedding generation, LLM inference, vector search) can run on powerful remote infrastructure, while the GUI client remains lightweight and runs on the investigator's local machine. This enables:
|
||||
- Render slowly
|
||||
- Show flat event lists with no context
|
||||
- Require manual correlation across sources
|
||||
- Have poor visualization and navigation
|
||||
*This is highly opinionated from personal experiences of the maintainer with multiple large commercial suites which werent able to deliver a satisfying result. I wont name any here, and exceptions may exist, but improvements are required here.*
|
||||
|
||||
- Scaling compute resources independently of workstations
|
||||
- Deployment in air-gapped labs
|
||||
- Efficient resource utilization (centralized compute nodes can serve multiple investigators)
|
||||
**Semeion's Solution:** Fast, interactive timeline with semantic clustering that shows patterns immediately.
|
||||
|
||||
### Large Communication Datasets
|
||||
|
||||
Modern investigations involve:
|
||||
|
||||
- Seized databases with millions of relevant artifacts
|
||||
- Multilingual content
|
||||
- Slang, code words, and poor machine translation
|
||||
- Hours spent reading irrelevant conversations
|
||||
|
||||
**Semeion's Solution:** Semantic search that understands meaning, handles multiple languages, and jumps directly to timeline context.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Two Entry Points
|
||||
|
||||
#### **Entry Point 1: Semantic content discovery → timeline analysis**
|
||||
|
||||
```bash
|
||||
1. Investigator searches: "discussions about file transfers"
|
||||
2. Semeion finds semantically relevant messages
|
||||
3. Click any result → Timeline centers on that timestamp
|
||||
4. See all activity ±2 hours: file access, USB connections, deletions
|
||||
5. Semantic clustering highlights: "File Exfiltration Sequence" pattern
|
||||
```
|
||||
|
||||
#### **Entry Point 2: timestamp → content correlation**
|
||||
|
||||
```bash
|
||||
1. Incident log shows: "Ransomware encrypted files at 2024-03-15 14:22:00"
|
||||
2. Navigate timeline to that timestamp
|
||||
3. Semantic clustering reveals:
|
||||
- Communication spike 30min before (cluster: "Coordination Activity")
|
||||
- Suspicious file downloads (cluster: "Malware Delivery")
|
||||
- Network connections (cluster: "C2 Communication")
|
||||
- Process executions leading to encryption
|
||||
4. Complete attack chain visible at a glance
|
||||
```
|
||||
|
||||
## Semantic Event Clustering
|
||||
|
||||
**Traditional Timeline View (Overwhelming):**
|
||||
|
||||
```bash
|
||||
14:33:15 - USB device connected
|
||||
14:33:18 - Chrome visited facebook.com
|
||||
14:33:22 - File modified: report.docx
|
||||
14:33:30 - File copied to USB: project_data.zip (2.3 GB)
|
||||
14:33:35 - File copied to USB: budget.xlsx
|
||||
14:33:40 - File deleted: project_data.zip
|
||||
14:33:45 - WhatsApp message sent
|
||||
...50 more individual events...
|
||||
```
|
||||
|
||||
**Semeion Clustered View (Clear Pattern):**
|
||||
|
||||
```bash
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 14:33:15-14:33:40 ⚠️ File Transfer (4) │ ← Click to expand
|
||||
│ Pattern: Data Exfiltration Sequence │
|
||||
│ USB connected → 2 files copied → source deleted │
|
||||
│ Related: Files mentioned in chat 2min earlier │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### How Clustering Works
|
||||
|
||||
1. **Temporal Proximity**: Events within configurable time window evaluated together
|
||||
2. **Semantic Similarity**: Vector embeddings detect conceptually-related events
|
||||
3. **Entity Linking**: Shared file names, IPs, usernames connect events
|
||||
4. **Pattern Templates**: Pre-defined sequences (USB exfiltration, login chains, etc.)
|
||||
5. **LLM Summarization** (Optional): Natural language cluster descriptions
|
||||
|
||||
## Architecture
|
||||
|
||||
### Desktop Application (PySide6)
|
||||
|
||||
```bash
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ PySide6 Desktop UI │
|
||||
│ ┌───────────────┐ ┌──────────────┐ ┌─────────────┐ │
|
||||
│ │ Timeline │ │ Semantic │ │ Artifact │ │
|
||||
│ │ View │ │ Search │ │ Detail │ │
|
||||
│ └───────────────┘ └──────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Analysis & Clustering Engine │
|
||||
│ • Event clustering algorithm │
|
||||
│ • Semantic similarity calculation │
|
||||
│ • Cross-artifact correlation │
|
||||
│ • Timeline rendering engine │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Data Layer │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Qdrant │ │ SQLite │ │ LLM API │ │
|
||||
│ │ (Vectors) │ │ (Metadata) │ │ (Optional) │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Deployment Options
|
||||
|
||||
| Configuration | Qdrant | Embedding | LLM | Use Case |
|
||||
|---------------|--------|-----------|-----|----------|
|
||||
| **Fully Local** | localhost | Ollama/local model | Ollama (optional) | Single investigator, offline |
|
||||
| **Airgapped Network** | Internal server | Internal server | Internal server | proper forensic environment |
|
||||
| **Hybrid** | Local | Local | Cloud API (optional) | investigative environment without focus on confidentiality |
|
||||
|
||||
## Artifact Types
|
||||
|
||||
### Searchable & Timeline-Enabled
|
||||
|
||||
| Type | Examples | Features |
|
||||
|------|----------|----------|
|
||||
| **Messages** | WhatsApp, Telegram, Signal, SMS | Semantic search, multilingual, timeline clustering |
|
||||
| **Browser** | Chrome, Firefox, Safari, Edge | History, downloads, searches—timeline correlation |
|
||||
| **Email** | Any email format (EML, MBOX, PST) | Searchable content, timeline placement |
|
||||
| **Documents** | PDF, Word, plain text | Content search, access time correlation |
|
||||
|
||||
### Timeline-Only (Context View)
|
||||
|
||||
| Type | Examples |
|
||||
|------|----------|
|
||||
| **File Events** | File creation, modification, deletion, access |
|
||||
| **Process Events** | Application launches, process creation |
|
||||
| **Network Events** | Connections, DNS queries |
|
||||
| **System Events** | Logs, authentication events, USB connections |
|
||||
|
||||
## Technical Stack
|
||||
|
||||
| Component | Technology | Purpose |
|
||||
|-----------|------------|---------|
|
||||
| **UI Framework** | PySide6 (Qt6) | Native desktop application |
|
||||
| **Timeline Rendering** | PyQtGraph or Plotly | High-performance visualization |
|
||||
| **Vector Database** | Qdrant | Semantic search and similarity |
|
||||
| **Metadata Storage** | SQLite | Fast local queries, metadata |
|
||||
| **LLM Interface** | OpenAI-compatible API | Optional: cluster summarization, query interpretation |
|
||||
| **Embedding** | Sentence Transformers / OpenAI | Multilingual vector generation |
|
||||
| **Forensic Parsing** | pytsk3, pyewf | Disk image processing |
|
||||
| **Language** | Python 3.13+ | Application logic |
|
||||
|
||||
## Data Model
|
||||
|
||||
### SemeionArtifact (Universal Schema, WiP)
|
||||
|
||||
Every artifact—regardless of source—conforms to a unified model:
|
||||
|
||||
```bash
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ SemeionArtifact │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Identity: id, case_id │
|
||||
│ Classification: artifact_class, source_platform │
|
||||
│ Temporal: timestamp (UTC normalized) │
|
||||
│ Actors: [{identifier, display_name, role}] │
|
||||
│ Content: text, semantic_text │
|
||||
│ Entities: indexed_entities[] (files, IPs, etc) │
|
||||
│ Hierarchy: parent_id, context_group │
|
||||
│ Location: url, path, title │
|
||||
│ Embeddings: semantic_vector (768-dim) │
|
||||
│ Source-Specific: message{}, browser{}, email{}, etc │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Cluster Model (WiP)
|
||||
|
||||
```bash
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ EventCluster │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ id: unique_cluster_id │
|
||||
│ time_range: (start_timestamp, end_timestamp) │
|
||||
│ events: [artifact_ids] │
|
||||
│ pattern_type: "file_exfiltration" | "communication" │
|
||||
│ confidence: 0.0-1.0 │
|
||||
│ summary: "USB transfer with deletion" │
|
||||
│ icon: "⚠️" | "💬" | "🌐" | etc │
|
||||
│ semantic_links: [related_cluster_ids] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## What Semeion Is NOT (yet)
|
||||
|
||||
| Not This | Why |
|
||||
|----------|-----|
|
||||
| **Forensic suite replacement** | Companion tool—use alongside Autopsy for acquisition |
|
||||
| **Reporting tool** | Timeline export for reports, but documentation happens in primary suite |
|
||||
| **AI evidence interpreter** | AI assists with search/clustering; investigator interprets evidence |
|
||||
|
||||
## Development Setup
|
||||
|
||||
## 🛠 Development Setup
|
||||
|
||||
This project uses [uv](https://github.com/astral-sh/uv) for fast dependency management and the modern "Src Layout" structure.
|
||||
This project uses [uv](https://github.com/astral-sh/uv) for dependency management.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.13+
|
||||
- [uv](https://github.com/astral-sh/uv) installed (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
|
||||
- [uv](https://github.com/astral-sh/uv) installed
|
||||
- requirements.txt
|
||||
|
||||
### Installation Steps
|
||||
|
||||
1. **Clone the repository**
|
||||
|
||||
```bash
|
||||
git clone <your-repo-url>
|
||||
cd gamayun
|
||||
```
|
||||
|
||||
2. **Create a virtual environment**
|
||||
This project requires Python 3.13.
|
||||
|
||||
```bash
|
||||
uv venv --python 3.13
|
||||
```
|
||||
|
||||
3. **Activate the environment**
|
||||
|
||||
- Linux/macOS:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
- Windows:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\activate
|
||||
```
|
||||
|
||||
4. **Install dependencies**
|
||||
This command installs locked dependencies and links the local `gamayun` package in editable mode.
|
||||
|
||||
```bash
|
||||
uv pip install -r requirements.txt -e .
|
||||
```
|
||||
|
||||
### Running the Application
|
||||
|
||||
You can execute the module directly:
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
python src/gamayun/main.py
|
||||
git clone https://git.cc24.dev/mstoeck3/semeion
|
||||
cd semeion
|
||||
|
||||
# Create virtual environment
|
||||
uv venv --python 3.13
|
||||
|
||||
# Activate environment
|
||||
source .venv/bin/activate # Linux/macOS
|
||||
# .venv\Scripts\activate # Windows
|
||||
|
||||
# Install dependencies
|
||||
uv pip install -r requirements.txt -e .
|
||||
|
||||
# Configure environment
|
||||
cp .env.example .env
|
||||
# Edit .env with your Qdrant and embedding endpoint configurations
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
### Running
|
||||
|
||||
```bash
|
||||
pytest
|
||||
semeion
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
## System Requirements
|
||||
|
||||
### Ingestion Pipeline
|
||||
### Minimum (Remote Processing)
|
||||
|
||||
```bash
|
||||
Raw Evidence Sources
|
||||
├─ Forensic Images (E01, DD, AFF4)
|
||||
├─ Timeline CSV (Timesketch format)
|
||||
└─ Loose Files (documents, logs, databases)
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Artifact Extraction │
|
||||
│ • pytsk3 (images) │
|
||||
│ • CSV parser │
|
||||
│ • File processors │
|
||||
└───────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Content Extraction │
|
||||
│ • PDF, DOCX, XLSX │
|
||||
│ • SQLite databases │
|
||||
│ • Text files │
|
||||
│ • OCR for images │
|
||||
└───────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Semantic Enrichment │
|
||||
│ • Classify type │
|
||||
│ • Extract entities │
|
||||
│ • Detect relationships │
|
||||
│ • Add metadata │
|
||||
└───────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Embedding Generation │
|
||||
│ → Remote/Local Service │
|
||||
└───────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Index in Qdrant │
|
||||
│ • Vector + Payload │
|
||||
│ • Create indexes │
|
||||
│ • Snapshot for audit │
|
||||
└────────────────────────┘
|
||||
```
|
||||
| Resource | Requirement |
|
||||
|----------|-------------|
|
||||
| CPU | 4 cores |
|
||||
| RAM | 8 GB |
|
||||
| Storage | Minimal (evidence stored elsewhere) |
|
||||
| GPU | Not required |
|
||||
| Network | Access to Qdrant and embedding endpoints (if remote) |
|
||||
|
||||
Reproducibility: Each ingestion run generates a manifest file containing:
|
||||
### Recommended (Local Processing)
|
||||
|
||||
- Source hashes (MD5/SHA256 of evidence)
|
||||
- Model versions (embedding model, LLM)
|
||||
- Configuration parameters
|
||||
- Processing statistics
|
||||
- Timestamp and operator ID
|
||||
| Resource | Requirement |
|
||||
|----------|-------------|
|
||||
| CPU | 8+ cores |
|
||||
| RAM | 16 GB (32 GB for large cases) |
|
||||
| Storage | SSD, sufficient for evidence & vectors |
|
||||
| GPU | Optional (improves embedding speed with local models) |
|
||||
| Network | Optional (fully offline capable) |
|
||||
|
||||
This manifest allows exact reproduction of the index from the same source data.
|
||||
## Project Status
|
||||
|
||||
### Query Execution Pipeline
|
||||
**Current Phase:** MVP Development - Timeline & Core Features
|
||||
|
||||
```bash
|
||||
Natural Language Query
|
||||
"bitcoin transaction after drug deal"
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ LLM Query Parser │
|
||||
│ → Remote/Local Service │
|
||||
│ Returns: JSON Plan │
|
||||
└───────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Query Plan Editor (UI) │
|
||||
│ • Review plan │
|
||||
│ • Adjust parameters │
|
||||
│ • Modify steps │
|
||||
│ • User approves │
|
||||
└───────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Search Orchestrator │
|
||||
│ • Execute Step 1 │
|
||||
│ • Extract timestamps │
|
||||
│ • Execute Step 2 │
|
||||
│ • Apply temporal logic │
|
||||
└───────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Correlation Engine │
|
||||
│ • Calculate proximity │
|
||||
│ • Weight scores │
|
||||
│ • Build relationships │
|
||||
└───────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ Results Presentation │
|
||||
│ • Timeline view │
|
||||
│ • Correlation graph │
|
||||
│ • Export options │
|
||||
└────────────────────────┘
|
||||
```
|
||||
**Roadmap:**
|
||||
|
||||
---
|
||||
1. ✅ Concept and architecture design
|
||||
2. ⬜ Core infrastructure
|
||||
- Unified artifact ingestion (WhatsApp, Chrome)
|
||||
- SQLite + Qdrant integration
|
||||
3. ⬜ Timeline visualization
|
||||
- High-performance rendering (target: <200ms for 100k events)
|
||||
- Multi-source swim lanes
|
||||
- Zoom/pan navigation
|
||||
4. ⬜ Semantic clustering
|
||||
- Temporal proximity grouping
|
||||
- Pattern template matching
|
||||
- Semantic similarity detection
|
||||
5. ⬜ Communication search
|
||||
- Multilingual embedding
|
||||
- Query interpretation
|
||||
- Search → timeline jump
|
||||
6. ⬜ Additional parsers (Telegram, Signal, etc.)
|
||||
7. ⬜ Export and reporting
|
||||
8. ⬜ Performance optimization & polish
|
||||
|
||||
## Technical Stack
|
||||
## Why Open Source Matters
|
||||
|
||||
### Core Technologies
|
||||
**Transparency for Court:**
|
||||
|
||||
| Component | Technology | Version | License | Purpose |
|
||||
|----------------------|---------------------------|-----------|--------------------------------|--------------------------------|
|
||||
| GUI Framework | PySide6 | TBD | LGPL | Desktop application interface |
|
||||
| Vector Database | Qdrant | 1.10+ | Apache 2.0 | Semantic search and storage |
|
||||
| LLM Inference | OpenAI-compatible API | various | various | Natural language understanding |
|
||||
| LLM Models | various | various | various | Query parsing |
|
||||
| Embeddings | OpenAI-compatible API | various | various | Semantic vectors |
|
||||
| Embedding Model | TBD | TBD | TBD | Text to vector conversion |
|
||||
| Forensic Parsing | pytsk3 | TBD | Apache 2.0 | Disk image processing |
|
||||
| Image Handling | pyewf | TBD | LGPL | E01 image support |
|
||||
| NLP | spaCy | TBD | MIT | Entity extraction |
|
||||
| Programming Language | Python | 3.13+ | PSF | Application logic |
|
||||
- Auditable algorithms—no "black box" analysis
|
||||
- Reproducible results—scientific validation possible
|
||||
- Peer-reviewed methods—community scrutiny
|
||||
|
||||
### Infrastructure Requirements
|
||||
**Accessibility:**
|
||||
|
||||
#### Remote Processing
|
||||
- Free for budget-constrained labs
|
||||
- No vendor lock-in
|
||||
- Community-driven development
|
||||
|
||||
- CPU: multi-Core
|
||||
- RAM: ~4GiB
|
||||
- Storage: insignificant, local client, possible evidence
|
||||
- GPU: irrelevant
|
||||
- remote infrastructure for compute
|
||||
**Innovation:**
|
||||
|
||||
#### Small scale local processing (local workstation)
|
||||
- Rapid feature development
|
||||
- Specialized extensions possible
|
||||
- Academic research enabled
|
||||
|
||||
- CPU: 8+ cores (modern Ryzen5/Ryzen7/Ryzen AI series)
|
||||
- RAM: 32GB minimum (16GB for Qdrant, 8GB for LLM, 8GB for OS/app)
|
||||
- Storage: 2TB SSD (1TB evidence + 1TB index)
|
||||
- GPU: recommended, not required (speed considerations)
|
||||
## Branding
|
||||
|
||||
#### Recommended Configuration (Remote Server Compute Node)
|
||||
**Semeion** derives from the Greek σημεῖον (sēmeîon), meaning "sign," "signal," or "meaningful mark"—embodying the software's mission of revealing meaningful patterns within digital evidence through intelligent timeline analysis.
|
||||
|
||||
- CPU: 16+ cores (Ryzen7+)
|
||||
- RAM: 64-128 GiB
|
||||
- Storage: sufficient for index+evidence
|
||||
- GPU: recommended, AMD Instinct MI50 32 GiB or better (Inference, Embeddings)
|
||||
- Network: sufficient
|
||||
|
||||
#### Enterprise Configuration (Multi-User)
|
||||
|
||||
- TBD, out of scope
|
||||
|
||||
---
|
||||
|
||||
## Supported Ingestion Formats
|
||||
|
||||
### Primary: Specialized Data Objects
|
||||
|
||||
TBD
|
||||
|
||||
### Secondary: Conversion Engine (algorithmic)
|
||||
|
||||
Example:
|
||||
|
||||
- SQLite Parser for browser History -> Special Data Object
|
||||
- Converter for TSK artifacts -> Metadata in Special Data Object (TBD)
|
||||
|
||||
## Use Case Scenarios
|
||||
|
||||
### Scenario 1: Drug Transaction Investigation
|
||||
|
||||
Query: "Find when the suspect made cryptocurrency payments after discussing deals" Process:
|
||||
|
||||
1. System finds chat messages about drug deals
|
||||
2. Extracts timestamps of deal discussions
|
||||
3. Searches for cryptocurrency-related activity after each discussion
|
||||
4. Correlates wallet launches, browser activity, blockchain transactions
|
||||
5. Presents timeline showing: discussion → wallet launch → transaction Evidence: Timeline of intent → action, strengthening case
|
||||
|
||||
### Scenario 2: Data Exfiltration
|
||||
|
||||
Query: "Show file access before large uploads to cloud storage" Process:
|
||||
|
||||
1. Identifies cloud storage upload events
|
||||
2. Looks backward in time for file access
|
||||
3. Correlates accessed files with uploaded data
|
||||
4. Maps file paths to user actions Evidence: Demonstrates what data was taken and when
|
||||
|
||||
### Scenario 3: Coordinated Activity
|
||||
|
||||
Query: "Find people who communicated privately and are also in the same groups" Process:
|
||||
|
||||
1. Extracts participants from private messages
|
||||
2. Extracts participants from group chats
|
||||
3. Identifies overlap (intersection)
|
||||
4. Shows sample conversations from each context Evidence: Demonstrates coordinated behavior across communication channels
|
||||
|
||||
### Scenario 4: Timeline Reconstruction
|
||||
|
||||
Query: "What happened between receiving the threatening email and deleting files?" Process:
|
||||
|
||||
1. Finds threatening email (semantic search)
|
||||
2. Finds file deletion events (system logs)
|
||||
3. Returns all artifacts between these timestamps
|
||||
4. Visualizes complete timeline Evidence: Establishes sequence of events and potential motive
|
||||
The mascot, **Koios** (Coeus), the Titan of intellect and inquiry in Greek mythology, represents the investigative mindset: the pursuit of hidden connections through analytical thought.
|
||||
|
||||
## License
|
||||
|
||||
BSD 3-Clause (subject to change during development)
|
||||
BSD 3-Clause
|
||||
|
||||
## Contributing
|
||||
|
||||
Semeion is in active development. Contributions welcome, especially from:
|
||||
|
||||
- Digital forensics practitioners (workflow validation)
|
||||
- Timeline visualization experts
|
||||
- Multilingual NLP specialists
|
||||
- Performance optimization engineers
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
[project]
|
||||
name = "gamayun"
|
||||
name = "semeion"
|
||||
version = "0.0.0"
|
||||
description = "Scalable vector search engine with focus on post-mortem forensics"
|
||||
authors = [
|
||||
{name = "Mario Stöckl", email = "mstoeck3@hs-mittweida.de"}
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = "~=3.13"
|
||||
requires-python = "~=3.13.0"
|
||||
license = {text = "BSD-3-Clause"}
|
||||
|
||||
dependencies = [
|
||||
"PySide6~=6.8.0",
|
||||
"qdrant_client~=1.16.1",
|
||||
"openai~=2.8.1",
|
||||
"python-dotenv~=1.2.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -23,7 +26,7 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/gamayun"]
|
||||
packages = ["src/semeion"]
|
||||
|
||||
[project.scripts]
|
||||
gamayun-cli = "gamayun.main:main"
|
||||
semeion = "semeion.main:main"
|
||||
|
||||
@@ -3,11 +3,15 @@
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.11.0
|
||||
# via httpx
|
||||
# via
|
||||
# httpx
|
||||
# openai
|
||||
certifi==2025.11.12
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
distro==1.9.0
|
||||
# via openai
|
||||
grpcio==1.76.0
|
||||
# via qdrant-client
|
||||
h11==0.16.0
|
||||
@@ -19,7 +23,9 @@ hpack==4.1.0
|
||||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via qdrant-client
|
||||
# via
|
||||
# openai
|
||||
# qdrant-client
|
||||
hyperframe==6.1.0
|
||||
# via h2
|
||||
idna==3.11
|
||||
@@ -28,8 +34,12 @@ idna==3.11
|
||||
# httpx
|
||||
iniconfig==2.3.0
|
||||
# via pytest
|
||||
jiter==0.12.0
|
||||
# via openai
|
||||
numpy==2.3.5
|
||||
# via qdrant-client
|
||||
openai==2.8.1
|
||||
# via semeion (pyproject.toml)
|
||||
packaging==25.0
|
||||
# via pytest
|
||||
pluggy==1.6.0
|
||||
@@ -39,13 +49,15 @@ portalocker==3.2.0
|
||||
protobuf==6.33.1
|
||||
# via qdrant-client
|
||||
pydantic==2.12.4
|
||||
# via qdrant-client
|
||||
# via
|
||||
# openai
|
||||
# qdrant-client
|
||||
pydantic-core==2.41.5
|
||||
# via pydantic
|
||||
pygments==2.19.2
|
||||
# via pytest
|
||||
pyside6==6.8.3
|
||||
# via gamayun (pyproject.toml)
|
||||
# via semeion (pyproject.toml)
|
||||
pyside6-addons==6.8.3
|
||||
# via pyside6
|
||||
pyside6-essentials==6.8.3
|
||||
@@ -53,19 +65,26 @@ pyside6-essentials==6.8.3
|
||||
# pyside6
|
||||
# pyside6-addons
|
||||
pytest==9.0.1
|
||||
# via gamayun (pyproject.toml)
|
||||
# via semeion (pyproject.toml)
|
||||
python-dotenv==1.2.1
|
||||
# via semeion (pyproject.toml)
|
||||
qdrant-client==1.16.1
|
||||
# via gamayun (pyproject.toml)
|
||||
# via semeion (pyproject.toml)
|
||||
shiboken6==6.8.3
|
||||
# via
|
||||
# pyside6
|
||||
# pyside6-addons
|
||||
# pyside6-essentials
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
# via
|
||||
# anyio
|
||||
# openai
|
||||
tqdm==4.67.1
|
||||
# via openai
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# grpcio
|
||||
# openai
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# typing-inspection
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 6.6 MiB |
BIN
resources/workflow.png
Normal file
BIN
resources/workflow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 MiB |
@@ -1,18 +0,0 @@
|
||||
"""
|
||||
Main entry point for the Gamayun forensic investigation application.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from gamayun.ui.main_window import MainWindow
|
||||
|
||||
|
||||
def main():
|
||||
# TODO: Initialize logging here
|
||||
# TODO: Initialize llm client and qdrant client
|
||||
# TODO: start main window
|
||||
exit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +0,0 @@
|
||||
from PySide6.QtWidgets import QMainWindow
|
||||
|
||||
class GUI(QMainWindow):
|
||||
def __init__(self):
|
||||
pass
|
||||
14
src/semeion/__init__.py
Normal file
14
src/semeion/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
"""semeion semantic post-mortem forensics"""
|
||||
|
||||
__version__ = "0.0.0"
|
||||
|
||||
from .main import main
|
||||
|
||||
__all__ = ["main"]
|
||||
12
src/semeion/ingestion_engines/__init__.py
Normal file
12
src/semeion/ingestion_engines/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
# from .browserSqlite import BrowserSqliteEngine
|
||||
# from .forensicImage import ForensicImageEngine
|
||||
|
||||
__all__ = []
|
||||
12
src/semeion/ingestion_engines/browserSqlite/__init__.py
Normal file
12
src/semeion/ingestion_engines/browserSqlite/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
# TODO: implement browser SQlite database converter -> semeion object
|
||||
# adapt browser2timesketch.py
|
||||
|
||||
__all__ = []
|
||||
11
src/semeion/ingestion_engines/forensicImage/__init__.py
Normal file
11
src/semeion/ingestion_engines/forensicImage/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
# TODO: use pytsk to import forensic images and convert to semeion objects
|
||||
|
||||
__all__ = []
|
||||
12
src/semeion/interfaces/__init__.py
Normal file
12
src/semeion/interfaces/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
# from .llm import LLMClient
|
||||
# from .qdrant import QdrantClient
|
||||
|
||||
__all__ = []
|
||||
11
src/semeion/interfaces/embeddings/__init__.py
Normal file
11
src/semeion/interfaces/embeddings/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
# TODO: implement connection to embeddings provider
|
||||
|
||||
__all__ = []
|
||||
11
src/semeion/interfaces/llm/__init__.py
Normal file
11
src/semeion/interfaces/llm/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
# TODO: implement connection to openAI compatible endpoint
|
||||
|
||||
__all__ = []
|
||||
47
src/semeion/interfaces/llm/llm_client.py
Normal file
47
src/semeion/interfaces/llm/llm_client.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
import dotenv
|
||||
import openai
|
||||
|
||||
class LLMInterface():
|
||||
"""
|
||||
provide a custom interface along with special methods relevant with the semeion application
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
openai.api_key = dotenv.get_key(".env", "LLM_API_KEY")
|
||||
openai.base_url = dotenv.get_key(".env", "LLM_API_BASE")
|
||||
print("CLNT:LLMCLIENT INITIALIZED:", dotenv.get_key(".env", "LLM_MODEL"), "@", dotenv.get_key(".env", "LLM_API_BASE"))
|
||||
|
||||
def simple_query(self, prompt: str):
|
||||
try:
|
||||
print("CLNT:LLMCLIENT CALL LLM", prompt)
|
||||
if dotenv.get_key(".env", "LLM_MODEL") is None:
|
||||
raise ValueError("CLNT:LLMCLIENT ERR: LLM MODEL not set")
|
||||
else:
|
||||
model = dotenv.get_key(".env", "LLM_MODEL")
|
||||
|
||||
response = openai.chat.completions.create(
|
||||
model=str(model),
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Hello! How are you doing today? Is there something I can help you with?"
|
||||
}
|
||||
],
|
||||
timeout=30
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
print(f"CLNT:LLMCLIENT ERR: {e}")
|
||||
raise
|
||||
11
src/semeion/interfaces/qdrant/__init__.py
Normal file
11
src/semeion/interfaces/qdrant/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
# TODO: implement qdrant connector
|
||||
|
||||
__all__ = ["QdrantInterface"]
|
||||
41
src/semeion/interfaces/qdrant/qdrant_client.py
Normal file
41
src/semeion/interfaces/qdrant/qdrant_client.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from qdrant_client import QdrantClient
|
||||
import dotenv
|
||||
|
||||
class QdrantInterface():
|
||||
"""
|
||||
provide a custom interface along with special methods relevant with the semeion application
|
||||
"""
|
||||
standard_vector_size = 1536
|
||||
|
||||
|
||||
def __init__(self):
|
||||
host = dotenv.get_key(".env", "QDRANT_HOST")
|
||||
port_str = dotenv.get_key(".env", "QDRANT_PORT")
|
||||
port = int(port_str) if port_str is not None else None
|
||||
api_key = dotenv.get_key(".env", "QDRANT_API_KEY")
|
||||
self.client = QdrantClient(host=host, port=port, api_key=api_key)
|
||||
|
||||
def get_collections(self):
|
||||
collections_obj = self.client.get_collections()
|
||||
collections = []
|
||||
for c in collections_obj.collections:
|
||||
collections.append(c.name)
|
||||
return collections
|
||||
|
||||
def create_semeion_collection(self, collection_name: str):
|
||||
self.client.create_collection(collection_name, vector_size=self.standard_vector_size) if not self.client.collection_exists(collection_name) else None
|
||||
return self.client.collection_exists(collection_name)
|
||||
|
||||
def submit(self, payload):
|
||||
pass
|
||||
|
||||
def check_health(self):
|
||||
pass
|
||||
20
src/semeion/main.py
Normal file
20
src/semeion/main.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
from .ui import MainWindow
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication()
|
||||
gui = MainWindow()
|
||||
gui.show()
|
||||
exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
41
src/semeion/models/__init__.py
Normal file
41
src/semeion/models/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from .semeionArtifact import SemeionArtifact, Actors, ActorsRole, Content, ChunkInfo, ContextGroup, ContextGroupType, Location, Ingestion, ArtifactClass
|
||||
from .semeionSearchObject import SemeionSearchObject, Filters, Options, Interpretation
|
||||
from .source_specific_models import AuthenticationEventMetadata, BrowserEventMetadata, DocumentMetadata, EmailMetadata, FileEventMetadata, MessageMetadata, NetworkEventMetadata, ProcessEventMetadata, RegistryEventMetadata, ScheduledTaskMetadata, SystemEventMetadata
|
||||
|
||||
__all__ = ["SemeionArtifact",
|
||||
"Actors",
|
||||
"ActorsRole",
|
||||
"Content",
|
||||
"ChunkInfo",
|
||||
"ContextGroup",
|
||||
"ContextGroupType",
|
||||
"Location",
|
||||
"Ingestion",
|
||||
"ArtifactClass",
|
||||
"ContextGroupType",
|
||||
"Location",
|
||||
"Ingestion",
|
||||
"ArtifactClass",
|
||||
"SemeionSearchObject",
|
||||
"AuthenticationEventMetadata",
|
||||
"BrowserEventMetadata",
|
||||
"DocumentMetadata",
|
||||
"EmailMetadata",
|
||||
"FileEventMetadata",
|
||||
"MessageMetadata",
|
||||
"NetworkEventMetadata",
|
||||
"ProcessEventMetadata",
|
||||
"RegistryEventMetadata",
|
||||
"ScheduledTaskMetadata",
|
||||
"SystemEventMetadata",
|
||||
"Filters",
|
||||
"Options",
|
||||
"Interpretation"]
|
||||
224
src/semeion/models/semeionArtifact.py
Normal file
224
src/semeion/models/semeionArtifact.py
Normal file
@@ -0,0 +1,224 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Annotated, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class ArtifactClass(str, Enum):
|
||||
"""
|
||||
the general class of the artifact which determines wether it can be searched for or not.
|
||||
this also affects how the artifact is processed during ingestion and search.
|
||||
|
||||
currently only major artifact classes will have semantic meaning associated to them
|
||||
and are searcheable, while others will only be stored for timeline context display.
|
||||
"""
|
||||
# searchable artifact classes:
|
||||
MESSAGE = "message"
|
||||
BROWSER_EVENT = "browser_event"
|
||||
EMAIL = "email"
|
||||
DOCUMENT = "document"
|
||||
|
||||
# non-searchable artifact classes:
|
||||
FILE_EVENT = "file_event"
|
||||
PROCESS_EVENT = "process_event"
|
||||
REGISTRY_EVENT = "registry_event"
|
||||
SYSTEM_EVENT = "system_event"
|
||||
NETWORK_EVENT = "network_event"
|
||||
AUTHENTICATION_EVENT = "authentication_event"
|
||||
SCHEDULED_TASK = "scheduled_task"
|
||||
|
||||
class ActorsRole(str, Enum):
|
||||
"""
|
||||
this will determine the role of the actor, which is mainly important for message filtering and later behavioral analysis.
|
||||
"""
|
||||
SENDER = "sender" # message, email
|
||||
RECEIVER = "receiver" # message, email
|
||||
PARTICIPANT = "participant" # document collaboration, chat groups?
|
||||
CREATOR = "creator" # document, file system
|
||||
OWNER = "owner" # filesystem objects
|
||||
INITIATOR = "initiator" # browser events, filesystem, network events
|
||||
TARGET = "target" # network events, authentication events
|
||||
|
||||
class Actors(BaseModel):
|
||||
"""
|
||||
the actor is any entity associated with some behavioral pattern.
|
||||
"""
|
||||
identifier: str # some unique identifier which is consistent across artifacts
|
||||
display_name: str # human readeable, needs to be parsed properly by ingestion module
|
||||
role: ActorsRole # see class
|
||||
|
||||
class Content(BaseModel):
|
||||
"""
|
||||
the most important field, this is what gets embedded.
|
||||
"""
|
||||
text: str
|
||||
truncated: bool = False # this should never happen, but in some scenario it might be useful for debugging or edge cases
|
||||
# not chunked: the chunking is handled at ingestion time and linked via chunk_info
|
||||
|
||||
class ChunkInfo(BaseModel):
|
||||
"""
|
||||
if an artifact is chunked, this contains information about the chunking.
|
||||
"""
|
||||
index: int # zero-based index of the chunk
|
||||
total: int # total number of chunks
|
||||
|
||||
class ContextGroupType(str, Enum):
|
||||
"""
|
||||
some artifacts can be aggregated into context groups, if they are semantically liked
|
||||
"""
|
||||
THREAD = "thread" # email threads, message threads
|
||||
SESSION = "session" # browser sessions, network sessions
|
||||
# not process trees: they are inherently linked by parent_id references
|
||||
# not file system directories: they are inherently linked by path and parent_id
|
||||
# no chunks: they are inherently linked by chunk_info
|
||||
|
||||
class ContextGroup(BaseModel):
|
||||
"""
|
||||
some artifacts can be aggregated into context groups, if they are semantically liked
|
||||
"""
|
||||
type: ContextGroupType
|
||||
id: str
|
||||
|
||||
class Location(BaseModel):
|
||||
"""
|
||||
any information about where the artifact was located.
|
||||
"""
|
||||
host: str | None = None # hostname or machine identifier
|
||||
path: str | None = None # file path or resource path
|
||||
url: str | None = None # URL of the resource
|
||||
title: str | None = None # mostly there to provide something human readable for browser events
|
||||
physical: str | None = None # physical location, e.g., GPS coordinates, if applicable
|
||||
|
||||
class Ingestion(BaseModel):
|
||||
"""
|
||||
metadata about the ingestion process.
|
||||
"""
|
||||
ingested_at: datetime
|
||||
source_file: str # the original source file from which this artifact was ingested
|
||||
ingestor_id: str # identifier of the ingestor used
|
||||
ingestor_version: str # version of the ingestor used
|
||||
|
||||
|
||||
# source-specifi classes are imported from submodules
|
||||
from .source_specific_models import *
|
||||
source_specific_models = Union[
|
||||
DocumentMetadata,
|
||||
EmailMetadata,
|
||||
MessageMetadata,
|
||||
FileEventMetadata,
|
||||
NetworkEventMetadata,
|
||||
ProcessEventMetadata,
|
||||
RegistryEventMetadata,
|
||||
ScheduledTaskMetadata,
|
||||
SystemEventMetadata,
|
||||
AuthenticationEventMetadata,
|
||||
BrowserEventMetadata
|
||||
]
|
||||
|
||||
class SemeionArtifact(BaseModel):
|
||||
"""
|
||||
standard artifact structure for the semeion application.
|
||||
|
||||
JSON representation:
|
||||
{
|
||||
"_schema_version": "1.1.0",
|
||||
"id": "string (uuid-v5)",
|
||||
"case_id": "string",
|
||||
"searchable": "bool",
|
||||
"artifact_class": "string (enum)",
|
||||
"source_platform": "string",
|
||||
"timestamp": "string (ISO8601 UTC)",
|
||||
"actors": [
|
||||
{
|
||||
"identifier": "string",
|
||||
"display_name": "string | null",
|
||||
"role": "string (enum)"
|
||||
}
|
||||
],
|
||||
"content": {
|
||||
"text": "string",
|
||||
"truncated": "bool"
|
||||
},
|
||||
"display_text": "string",
|
||||
"indexed_entities": ["string"],
|
||||
"parent_id": "string (uuid) | null",
|
||||
"chunk_info": {
|
||||
"index": "int",
|
||||
"total": "int"
|
||||
} | null,
|
||||
"context_group": {
|
||||
"type": "string (enum)",
|
||||
"id": "string"
|
||||
} | null,
|
||||
"location": {
|
||||
"host": "string | null",
|
||||
"path": "string | null",
|
||||
"url": "string | null",
|
||||
"title": "string | null"
|
||||
} | null,
|
||||
"source_specific": {} | null,
|
||||
"marked_interesting": "bool",
|
||||
"marked_not_interesting": "bool",
|
||||
"ingestion": {
|
||||
"ingested_at": "string (ISO8601)",
|
||||
"source_file": "string",
|
||||
"parser_id": "string",
|
||||
"parser_version": "string"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
_schema_version: str = "1.1.1"
|
||||
id: UUID = Field(description="deterministic UUID v5 based on case_id, source_file, and unique key")
|
||||
case_id: str = Field(description="case identifier this artifact belongs to")
|
||||
searchable: bool = Field(description="searchable or only for timeline context")
|
||||
artifact_class: ArtifactClass = Field(description="general class of the artifact")
|
||||
source_platform: str = Field(description="source platform from which this artifact was ingested (sleuthkit, chromium SQLite, gecko SQLite, etc.)")
|
||||
timestamp: datetime = Field(description="timestamp of the artifact in ISO8601 UTC format - the main timestamp which will be used for searching and timeline ordering")
|
||||
actors: list[Actors] = Field(description="of actors associated with this artifact")
|
||||
content: Content = Field(description="main content of the artifact which will be embedded for semantic search")
|
||||
display_text: str = Field(description="human readable representation of the artifact content for displaying in the UI")
|
||||
indexed_entities: list[str] = Field(description="list of entities from the artifact content, might use sparse vectors for hybrid search")
|
||||
parent_id: UUID | None = Field(default=None, description="serves for any nested relationships, e.g., file system hierarchies, process trees, etc.")
|
||||
chunk_info: ChunkInfo | None = Field(default=None, description="if the artifact content is chunked, this contains information about the chunking")
|
||||
context_group: ContextGroup | None = Field(default=None, description="a parameter which provides information for semantically linked artifacts, e.g., email threads, browser sessions, etc.")
|
||||
location: Location | None = Field(default=None, description="information about where the artifact was located, e.g., file path, URL, host, etc.")
|
||||
source_specific: source_specific_models | None = Field(default=None, description="source-specific metadata for the artifact")
|
||||
marked_interesting: bool = Field(description="user feedback flag marking this artifact as interesting")
|
||||
marked_not_interesting: bool = Field(description="user feedback flag marking this artifact as not interesting")
|
||||
ingestion: Ingestion = Field(description="metadata about the ingestion process")
|
||||
|
||||
class Config:
|
||||
"""
|
||||
this is necessary for pydantic
|
||||
"""
|
||||
use_enum_values = True
|
||||
populate_by_name = True
|
||||
validate_assignment = True
|
||||
|
||||
def is_searchable(self) -> bool:
|
||||
if self.artifact_class in [ArtifactClass.MESSAGE, ArtifactClass.BROWSER_EVENT, ArtifactClass.EMAIL, ArtifactClass.DOCUMENT] and self.searchable:
|
||||
return True
|
||||
elif self.artifact_class in [ArtifactClass.MESSAGE, ArtifactClass.BROWSER_EVENT, ArtifactClass.EMAIL, ArtifactClass.DOCUMENT] and not self.searchable:
|
||||
exception = f"Artifact {self.id} of class {self.artifact_class} is marked as non-searchable, but this class should always be searchable."
|
||||
raise ValueError(exception)
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_vector_payload(self) -> dict:
|
||||
"""
|
||||
submit to qdrant
|
||||
"""
|
||||
payload = self.model_dump(mode="json", by_alias=True)
|
||||
return payload
|
||||
|
||||
92
src/semeion/models/semeionSearchObject.py
Normal file
92
src/semeion/models/semeionSearchObject.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
# from .llm import LLMClient
|
||||
# from .qdrant import QdrantClient
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class Filters(BaseModel):
|
||||
"""
|
||||
filters which can be applied to the search query to limit the search space.
|
||||
all filters are optional and can be combined.
|
||||
"""
|
||||
case_ids: list[str] | None = Field(default=None, description="list of case IDs to limit the search to")
|
||||
artifact_classes: list[str] | None = Field(default=None, description="list of artifact classes to limit the search to")
|
||||
source_platforms: list[str] | None = Field(default=None, description="list of source platforms to limit the search to")
|
||||
actor_identifiers: list[str] | None = Field(default=None, description="list of actor identifiers to limit the search to")
|
||||
indexed_entities_any: list[str] | None = Field(default=None, description="list of indexed entities, any of which must be present in the artifact")
|
||||
time_after: datetime | None = Field(default=None, description="only return artifacts after this timestamp (inclusive)")
|
||||
time_before: datetime | None = Field(default=None, description="only return artifacts before this timestamp (inclusive)")
|
||||
context_group_ids: list[str] | None = Field(default=None, description="list of context group IDs to limit the search to")
|
||||
hosts: list[str] | None = Field(default=None, description="list of hostnames or IPs to limit the search to")
|
||||
|
||||
class Options(BaseModel):
|
||||
"""
|
||||
options to modify the search behavior.
|
||||
"""
|
||||
limit: int = Field(default=100, description="maximum number of search results to return")
|
||||
min_score: float | None = Field(default=None, description="minimum similarity score threshold for returned results")
|
||||
use_hybrid: bool = Field(default=False, description="whether to use hybrid search (semantic dense vectors + keyword (sparse vectors)")
|
||||
|
||||
class Interpretation(BaseModel):
|
||||
"""
|
||||
this data structure holds information about how the semantic query was interpreted by the generative LLM
|
||||
"""
|
||||
original_query: str = Field(description="the original user query before any modifications or interpretations")
|
||||
notes: list[str] = Field(default=[], description="any notes or clarifications made by the LLM regarding the query interpretation")
|
||||
|
||||
class SemeionSearchObject(BaseModel):
|
||||
"""
|
||||
standard search object structure for the semeion application.
|
||||
|
||||
example JSON representation:
|
||||
{
|
||||
"_schema_version": "1.1.0",
|
||||
|
||||
"query_id": "string (uuid-v4)",
|
||||
"created_at": "string (ISO8601)",
|
||||
|
||||
"semantic_query": "string",
|
||||
|
||||
"filters": {
|
||||
"case_ids": ["string"] | null,
|
||||
"artifact_classes": ["string"] | null,
|
||||
"source_platforms": ["string"] | null,
|
||||
"actor_identifiers": ["string"] | null,
|
||||
"indexed_entities_any": ["string"] | null,
|
||||
"time_after": "string (ISO8601)" | null,
|
||||
"time_before": "string (ISO8601)" | null,
|
||||
"context_group_ids": ["string"] | null,
|
||||
"hosts": ["string"] | null
|
||||
},
|
||||
|
||||
"options": {
|
||||
"limit": "int",
|
||||
"min_score": "float" | null,
|
||||
"use_hybrid": "bool"
|
||||
},
|
||||
|
||||
"interpretation": {
|
||||
"original_query": "string",
|
||||
"notes": ["string"]
|
||||
}
|
||||
}
|
||||
"""
|
||||
_schema_version: str = "1.1.0"
|
||||
query_id: UUID = Field(description="UUID v4 identifying this search query")
|
||||
created_at: datetime = Field(description="timestamp when the search query was created for audit")
|
||||
semantic_query: str = Field(description="the search string which gets embedded and used for semantic search")
|
||||
filters: Filters = Field(description="filters to limit the search space")
|
||||
options: Options = Field(description="options to modify the search behavior")
|
||||
interpretation: Interpretation = Field(description="information about how the semantic query was interpreted by the generative LLM")
|
||||
|
||||
|
||||
21
src/semeion/models/source_specific_models/__init__.py
Normal file
21
src/semeion/models/source_specific_models/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from .document_metadata import DocumentMetadata
|
||||
from .email import EmailMetadata
|
||||
from .message import MessageMetadata
|
||||
from .file_event import FileEventMetadata
|
||||
from .network_event import NetworkEventMetadata
|
||||
from .process_event import ProcessEventMetadata
|
||||
from .registry_event import RegistryEventMetadata
|
||||
from .scheduled_task import ScheduledTaskMetadata
|
||||
from .system_event import SystemEventMetadata
|
||||
from .authentication_event import AuthenticationEventMetadata
|
||||
from .browser_event import BrowserEventMetadata
|
||||
|
||||
__all__ = ["DocumentMetadata", "EmailMetadata", "MessageMetadata", "FileEventMetadata", "NetworkEventMetadata", "ProcessEventMetadata", "RegistryEventMetadata", "ScheduledTaskMetadata", "SystemEventMetadata", "AuthenticationEventMetadata", "BrowserEventMetadata"]
|
||||
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AuthenticationEventMetadata(BaseModel):
|
||||
"""
|
||||
metadata for authentication events, e.g. ssh logins, user logins, etc.
|
||||
"""
|
||||
pass
|
||||
15
src/semeion/models/source_specific_models/browser_event.py
Normal file
15
src/semeion/models/source_specific_models/browser_event.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class BrowserEventMetadata(BaseModel):
|
||||
"""
|
||||
metadata for browser events, e.g. history, bookmarks, downloads, etc.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class DocumentMetadata(BaseModel):
|
||||
"""
|
||||
metadata for documents, e.g. PDFs, Word files, etc.
|
||||
"""
|
||||
pass
|
||||
15
src/semeion/models/source_specific_models/email.py
Normal file
15
src/semeion/models/source_specific_models/email.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class EmailMetadata(BaseModel):
|
||||
"""
|
||||
metadata for emails
|
||||
"""
|
||||
pass
|
||||
15
src/semeion/models/source_specific_models/file_event.py
Normal file
15
src/semeion/models/source_specific_models/file_event.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class FileEventMetadata(BaseModel):
|
||||
"""
|
||||
metadata for file events, e.g. file creation, modification, deletion, filetype, etc.
|
||||
"""
|
||||
pass
|
||||
15
src/semeion/models/source_specific_models/message.py
Normal file
15
src/semeion/models/source_specific_models/message.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class MessageMetadata(BaseModel):
|
||||
"""
|
||||
metadata for messages (emails, chats, etc.)
|
||||
"""
|
||||
pass
|
||||
15
src/semeion/models/source_specific_models/network_event.py
Normal file
15
src/semeion/models/source_specific_models/network_event.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class NetworkEventMetadata(BaseModel):
|
||||
"""
|
||||
metadata for network events, e.g. connections, data transfers, etc.
|
||||
"""
|
||||
pass
|
||||
15
src/semeion/models/source_specific_models/process_event.py
Normal file
15
src/semeion/models/source_specific_models/process_event.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ProcessEventMetadata(BaseModel):
|
||||
"""
|
||||
metadata for process events, e.g. process start, stop, fork, etc.
|
||||
"""
|
||||
pass
|
||||
15
src/semeion/models/source_specific_models/registry_event.py
Normal file
15
src/semeion/models/source_specific_models/registry_event.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class RegistryEventMetadata(BaseModel):
|
||||
"""
|
||||
metadata for registry events, e.g. key creation, modification, deletion, etc.
|
||||
"""
|
||||
pass
|
||||
15
src/semeion/models/source_specific_models/scheduled_task.py
Normal file
15
src/semeion/models/source_specific_models/scheduled_task.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ScheduledTaskMetadata(BaseModel):
|
||||
"""
|
||||
metadata for scheduled tasks, e.g. cron jobs, automated scripts, etc.
|
||||
"""
|
||||
pass
|
||||
15
src/semeion/models/source_specific_models/system_event.py
Normal file
15
src/semeion/models/source_specific_models/system_event.py
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class SystemEventMetadata(BaseModel):
|
||||
"""
|
||||
metadata for system events, e.g. system start, shutdown, errors, etc.
|
||||
"""
|
||||
pass
|
||||
13
src/semeion/services/__init__.py
Normal file
13
src/semeion/services/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from semeion.services.llm_service import LLMService
|
||||
from semeion.services.qdrant_service import QdrantService
|
||||
from semeion.services.localdb import DatabaseService, AppSettings
|
||||
|
||||
__all__ = ["LLMService", "QdrantService", "DatabaseService", "AppSettings"]
|
||||
76
src/semeion/services/llm_service.py
Normal file
76
src/semeion/services/llm_service.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from semeion.interfaces.llm.llm_client import LLMInterface
|
||||
from PySide6.QtCore import QObject, Signal, Slot, QThread
|
||||
|
||||
|
||||
class LLMWorker(QObject): # initializes once and is queried repeatedly
|
||||
result = Signal(object)
|
||||
error = Signal(str)
|
||||
status = Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.client = None
|
||||
|
||||
@Slot()
|
||||
def initialize(self):
|
||||
self.client = LLMInterface()
|
||||
self.status.emit("LLM: ready")
|
||||
|
||||
@Slot(str)
|
||||
def run_query(self, prompt: str):
|
||||
if self.client is None:
|
||||
self.error.emit("LLM client not initialized")
|
||||
return
|
||||
try:
|
||||
self.status.emit("LLM: started")
|
||||
raw_response = self.client.simple_query(prompt)
|
||||
response = raw_response.choices[0].message.content or ""
|
||||
self.status.emit("LLM: finished")
|
||||
self.result.emit(response)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
class LLMService(QObject):
|
||||
"""persistent service"""
|
||||
query_finished = Signal(object)
|
||||
query_error = Signal(str)
|
||||
query_status = Signal(str)
|
||||
|
||||
# signal to send to active worker
|
||||
_do_query = Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._thread = QThread()
|
||||
self.worker = LLMWorker()
|
||||
|
||||
self.worker.moveToThread(self._thread)
|
||||
|
||||
self._thread.started.connect(self.worker.initialize)
|
||||
|
||||
self._do_query.connect(self.worker.run_query)
|
||||
|
||||
self.worker.result.connect(self.query_finished.emit)
|
||||
self.worker.error.connect(self.query_error.emit)
|
||||
self.worker.status.connect(self.query_status.emit)
|
||||
|
||||
self._thread.start()
|
||||
|
||||
@Slot(str)
|
||||
def query(self, prompt: str):
|
||||
self._do_query.emit(prompt) # start query via signal
|
||||
|
||||
def stop(self):
|
||||
self._thread.quit()
|
||||
self._thread.wait()
|
||||
self.worker.deleteLater()
|
||||
self._thread.deleteLater()
|
||||
84
src/semeion/services/localdb.py
Normal file
84
src/semeion/services/localdb.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
from dotenv import dotenv_values
|
||||
DB_DIR = "app_database"
|
||||
DB_FILE = os.path.join(DB_DIR, "management_db.sqlite")
|
||||
|
||||
class DatabaseService():
|
||||
def __init__(self) -> None:
|
||||
print("DBSV: INITIALIZE DATABASE")
|
||||
self.conn = self.connect_db()
|
||||
|
||||
def connect_db(self) -> sqlite3.Connection:
|
||||
os.makedirs(DB_DIR, exist_ok=True)
|
||||
try:
|
||||
conn = sqlite3.connect(DB_FILE)
|
||||
print("DBSV: DATABASE AVAILABLE")
|
||||
return conn
|
||||
except sqlite3.Error as e:
|
||||
raise
|
||||
|
||||
def check_exist(self) -> bool:
|
||||
if os.path.exists(DB_FILE):
|
||||
print("DBSV: DATABASE EXISTS")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
class AppSettings(DatabaseService):
|
||||
def __init__(self)->None:
|
||||
print("DBSV:APPSETTINGS: SETTINGS AVAILABLE")
|
||||
self.conn = self.connect_db()
|
||||
self.create_initial_configs()
|
||||
|
||||
def check_exist(self):
|
||||
try:
|
||||
with self.conn as conn:
|
||||
cur = conn.cursor()
|
||||
statement = """
|
||||
SELECT key,val FROM settings;
|
||||
"""
|
||||
cur.execute(statement)
|
||||
rows = cur.fetchall()
|
||||
if len(rows)==6: # expecting 6 initial settings
|
||||
print("DBSV:APPSETTINGS: SETTINGS EXIST")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except sqlite3.DatabaseError as e:
|
||||
print("DBSV: DB NOT FOUND. CREATING...")
|
||||
return e
|
||||
|
||||
def create_initial_configs(self)->None:
|
||||
if self.check_exist()==True:
|
||||
|
||||
return
|
||||
else:
|
||||
print("DBSV:APPSETTINGS: CREATING INITIAL SETTINGS")
|
||||
initial_config =dotenv_values()
|
||||
with self.conn as conn:
|
||||
cur = conn.cursor()
|
||||
statement = """
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key text NOT NULL,
|
||||
val text NOT NULL
|
||||
);
|
||||
"""
|
||||
cur.execute(statement)
|
||||
for key,val in initial_config.items():
|
||||
task = (key,val)
|
||||
statement = f"""
|
||||
INSERT INTO settings(key,val) VALUES(?,?);
|
||||
"""
|
||||
cur.execute(statement, task)
|
||||
conn.commit()
|
||||
|
||||
67
src/semeion/services/qdrant_service.py
Normal file
67
src/semeion/services/qdrant_service.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
from semeion.interfaces.qdrant.qdrant_client import QdrantInterface
|
||||
from PySide6.QtCore import QObject, Signal, Slot, QThread
|
||||
|
||||
class QdrantWorker(QObject):
|
||||
result = Signal(object)
|
||||
error = Signal(str)
|
||||
status = Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.client = None
|
||||
|
||||
@Slot()
|
||||
def initialize(self):
|
||||
self.client = QdrantInterface()
|
||||
self.status.emit("Qdrant: ready")
|
||||
|
||||
@Slot()
|
||||
def get_collections(self):
|
||||
try:
|
||||
if self.client is None:
|
||||
self.error.emit("Qdrant: Failed to establish connection to QDrant")
|
||||
return
|
||||
self.status.emit("Qdrant: fetching collections")
|
||||
collections = self.client.get_collections()
|
||||
self.status.emit("Qdrant: Successfully fetched collections")
|
||||
self.result.emit(collections)
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
|
||||
|
||||
# Service = manages Worker + QThread, UI connects to this
|
||||
class QdrantService(QObject):
|
||||
query_finished = Signal(object)
|
||||
query_error = Signal(str)
|
||||
query_status = Signal(str)
|
||||
|
||||
_do_discover = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._thread = QThread()
|
||||
self.worker =QdrantWorker()
|
||||
|
||||
self.worker.moveToThread(self._thread)
|
||||
|
||||
self._thread.started.connect(self.worker.initialize)
|
||||
|
||||
self._do_discover.connect(self.worker.get_collections)
|
||||
|
||||
self.worker.result.connect(self.query_finished.emit)
|
||||
self.worker.error.connect(self.query_error.emit)
|
||||
self.worker.status.connect(self.query_status.emit)
|
||||
|
||||
self._thread.start()
|
||||
|
||||
@Slot()
|
||||
def discover_collections(self):
|
||||
self._do_discover.emit()
|
||||
10
src/semeion/ui/__init__.py
Normal file
10
src/semeion/ui/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
from .main_window import MainWindow
|
||||
|
||||
__all__ = ["MainWindow"]
|
||||
145
src/semeion/ui/main_window.py
Normal file
145
src/semeion/ui/main_window.py
Normal file
@@ -0,0 +1,145 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLabel, QPushButton, QLineEdit, QMenu, QHBoxLayout
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from . import styling
|
||||
from semeion.services import LLMService, QdrantService, DatabaseService, AppSettings
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
main_exec_start = Signal(str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.db = DatabaseService()
|
||||
self.settings = AppSettings()
|
||||
print("MAIN: CREATING SERVICES")
|
||||
self.create_services()
|
||||
print("MAIN: INITIALIZING GUI")
|
||||
self.create_ui()
|
||||
self.connect_ui_elements()
|
||||
|
||||
|
||||
def create_ui(self):
|
||||
self.setWindowTitle("Semeion Interface")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
self.setStyleSheet(styling.main_window_style)
|
||||
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
self.main_layout = QVBoxLayout()
|
||||
central_widget.setLayout(self.main_layout)
|
||||
|
||||
# Toolbar
|
||||
self.create_menuBar()
|
||||
|
||||
self.conn_status_layout = QHBoxLayout()
|
||||
conn_widget_qdrant = QLabel("Qdrant Interface available")
|
||||
conn_widget_llm = QLabel("LLM Interface available")
|
||||
conn_widget_embeddings = QLabel("EMB_PLACEHOLDER")
|
||||
status_widgets = []
|
||||
status_widgets.append(conn_widget_qdrant)
|
||||
status_widgets.append(conn_widget_llm)
|
||||
status_widgets.append(conn_widget_embeddings)
|
||||
for w in status_widgets:
|
||||
w.setStyleSheet(styling.status_widget_style)
|
||||
self.conn_status_layout.addWidget(w)
|
||||
self.main_layout.addLayout(self.conn_status_layout)
|
||||
|
||||
|
||||
# spacing
|
||||
self.main_layout.addStretch(1)
|
||||
|
||||
# branding
|
||||
self.brandingLabel = QLabel("S E M E I O N\n")
|
||||
self.brandingLabel.setStyleSheet("font-size: 32px; font-weight: bold; font-family: Cantarell;")
|
||||
self.main_layout.addWidget(self.brandingLabel, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# search Input
|
||||
self.searchInput = QLineEdit()
|
||||
self.searchInput.setPlaceholderText("Natural Language Search...")
|
||||
self.searchInput.setFixedWidth(400)
|
||||
self.searchInput.setMinimumHeight(30)
|
||||
self.searchInput.setStyleSheet(styling.input_style)
|
||||
self.main_layout.addWidget(self.searchInput, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
self.main_layout.addSpacing(10)
|
||||
|
||||
self.executeButton = QPushButton("Execute")
|
||||
self.executeButton.setAttribute(Qt.WidgetAttribute.WA_Hover)
|
||||
self.executeButton.setStyleSheet(styling.button_style)
|
||||
self.main_layout.addWidget(self.executeButton, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
self.main_layout.addStretch(1)
|
||||
|
||||
def create_menuBar(self):
|
||||
menuBar = self.menuBar()
|
||||
action_caseManager = menuBar.addAction("&Case Manager", self.open_case_manager)
|
||||
action_caseManager.setStatusTip("Manage Cases")
|
||||
action_ingestionManager = menuBar.addAction("&Ingestion Manager", self.open_ingestion_manager)
|
||||
action_ingestionManager.setStatusTip("Ingest various Artifacts into dataset")
|
||||
menuBar_settingsMenu = QMenu("&Settings", self)
|
||||
action_connectionManager = menuBar_settingsMenu.addAction("&Manage Server Connections", self.open_connection_manager)
|
||||
menuBar.addMenu(menuBar_settingsMenu)
|
||||
menuBar_settingsMenu.setStatusTip("Application Settings")
|
||||
menuBar_about = QMenu("&About", self)
|
||||
action_aboutModal = menuBar_about.addAction("&About...", self.open_about_window)
|
||||
menuBar.addMenu(menuBar_about)
|
||||
menuBar.setStyleSheet(styling.toolbar_style)
|
||||
|
||||
def create_services(self):
|
||||
self.llm_service = LLMService()
|
||||
self.qdrant_service = QdrantService()
|
||||
|
||||
# Button -> executes LLM Service
|
||||
self.main_exec_start.connect(self.llm_service.query)
|
||||
# response handling
|
||||
self.llm_service.query_finished.connect(self.handle_llm_response)
|
||||
|
||||
# connect qdrant signals
|
||||
self.qdrant_service.query_finished.connect(self.handle_qdrant_response)
|
||||
# Qdrant: execute collection discovery on startup
|
||||
self.qdrant_service.discover_collections()
|
||||
|
||||
def connect_ui_elements(self):
|
||||
self.searchInput.returnPressed.connect(self.execute_from_input)
|
||||
self.executeButton.clicked.connect(self.execute_from_input)
|
||||
|
||||
def execute_from_input(self) -> str|None:
|
||||
query = self.searchInput.text().strip()
|
||||
if not query:
|
||||
return
|
||||
self.main_exec_start.emit(query)
|
||||
self.executeButton.setDisabled(True)
|
||||
self.searchInput.setDisabled(True)
|
||||
self.reenable_list = [self.executeButton, self.searchInput]
|
||||
print("MAIN: LLM QUERY SUBMIT:", query)
|
||||
|
||||
def handle_llm_response(self, response):
|
||||
self.enable_input(self.reenable_list)
|
||||
print("MAIN: LLM RESPONSE:", response)
|
||||
|
||||
def handle_qdrant_response(self, response):
|
||||
print("MAIN: QDRANT COLLECTIONS:", response)
|
||||
|
||||
def enable_input(self, elements: list):
|
||||
for e in elements:
|
||||
e.setEnabled(True)
|
||||
|
||||
def open_case_manager(self):
|
||||
print("clicked case manager")
|
||||
|
||||
def open_ingestion_manager(self):
|
||||
print("clicked ingestion manager")
|
||||
|
||||
def open_connection_manager(self):
|
||||
print("clicked open connection manager")
|
||||
|
||||
def open_about_window(self):
|
||||
print("clicked open about")
|
||||
|
||||
75
src/semeion/ui/styling/__init__.py
Normal file
75
src/semeion/ui/styling/__init__.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#
|
||||
# Copyright (c) 2025, mstoeck3
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the BSD-3-Clause license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
|
||||
## DISCLAIMER: Styles are mostly AI-generated
|
||||
|
||||
main_window_style = """
|
||||
QWidget {
|
||||
background-color: #f0f0f0;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
"""
|
||||
|
||||
button_style = """
|
||||
QPushButton {
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: #808080;
|
||||
color: white;
|
||||
border: 1px solid #6e6e6e;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #6e6e6e;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #5e5e5e;
|
||||
}
|
||||
"""
|
||||
|
||||
input_style = """
|
||||
QLineEdit {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 24px;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border: 1px solid #aaa;
|
||||
outline: none;
|
||||
}
|
||||
"""
|
||||
|
||||
toolbar_style = """
|
||||
QMenuBar::item {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QMenuBar::item:selected {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
QMenu {
|
||||
border: 2px solid #c0c0c0;
|
||||
border-radius: 6px;
|
||||
padding: 6px 4px;
|
||||
}
|
||||
QMenu::item {
|
||||
padding: 8px 24px 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 2px;
|
||||
}
|
||||
QMenu::item:selected {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
"""
|
||||
|
||||
status_widget_style = """
|
||||
|
||||
"""
|
||||
518
uv.lock
generated
Normal file
518
uv.lock
generated
Normal file
@@ -0,0 +1,518 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = "==3.13.*"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grpcio"
|
||||
version = "1.76.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "hpack" },
|
||||
{ name = "hyperframe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hpack"
|
||||
version = "4.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
http2 = [
|
||||
{ name = "h2" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyperframe"
|
||||
version = "6.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
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 = "jiter"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.3.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/e4/42591e356f1d53c568418dc7e30dcda7be31dd5a4d570bca22acb0525862/openai-2.8.1.tar.gz", hash = "sha256:cb1b79eef6e809f6da326a7ef6038719e35aa944c42d081807bfa1be8060f15f", size = 602490, upload-time = "2025-11-17T22:39:59.549Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
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 = "portalocker"
|
||||
version = "3.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.33.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyside6"
|
||||
version = "6.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyside6-addons" },
|
||||
{ name = "pyside6-essentials" },
|
||||
{ name = "shiboken6" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/5f/dc408fb62a99d20e30f930f4de60c7d5c257d25a97baab600dc430f859bf/PySide6-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:31f390c961b54067ae41360e5ea3b340ce0e0e5feadea2236c28226d3b37edcc", size = 553886, upload-time = "2025-03-27T12:10:10.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/67c41f7280ed9d1c53a50bdaa5a6050134875341f0be96a58d329fe71ade/PySide6-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8e53e2357bfbdee1fa86c48312bf637460a2c26d49e7af0b3fae2e179ccc7052", size = 554788, upload-time = "2025-03-27T12:10:12.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/80/79df5af0f309f75297054caf127e82c6c4e6c3cd237a8abd428d18fe3a16/PySide6-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:5bf5153cab9484629315f57c56a9ad4b7d075b4dd275f828f7549abf712c590b", size = 554785, upload-time = "2025-03-27T12:10:14.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/b0/238abab3e005fff130a455ba07e8f1323e624b14e41f70f2f62c6bdc2601/PySide6-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:722dc0061d8ef6dbb8c0b99864f21e83a5b49ece1ecb2d0b890840d969e1e461", size = 561080, upload-time = "2025-03-27T12:10:15.467Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyside6-addons"
|
||||
version = "6.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyside6-essentials" },
|
||||
{ name = "shiboken6" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/23/fcd006128ecac40284fc218f804a46c52ebc1ad5995f3edb548d296f0d39/PySide6_Addons-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:ea46649e40b9e6ab11a0da2da054d3914bff5607a5882885e9c3bc2eef200036", size = 302447038, upload-time = "2025-03-27T12:11:29.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/93/e7c743e7a01e66f22cac4133c320832700b9d15a43d9a46dbd067cc1877e/PySide6_Addons-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6983d3b01fad53637bad5360930d5923509c744cc39704f9c1190eb9934e33da", size = 160547187, upload-time = "2025-03-27T12:12:13.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/5b/51217c90f9292651ef39ec3179bcb7797db1481a8f61138378720b1b91bc/PySide6_Addons-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:7949a844a40ee10998eb2734e2c06c4c7182dfcd4c21cc4108a6b96655ebe59f", size = 156291508, upload-time = "2025-03-27T12:12:52.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/e8/bbe84519355814166bbb7e867dfc8c7ea0044827d64b6b11c29a7b1a7424/PySide6_Addons-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:67548f6db11f4e1b7e4b6efd9c3fc2e8d275188a7b2feac388961128572a6955", size = 127865208, upload-time = "2025-03-27T12:13:24.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyside6-essentials"
|
||||
version = "6.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "shiboken6" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/74/082381f996baea3d6716d542c2184cb031e63f1cb6d4edca871fe82dd787/PySide6_Essentials-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:aa56c135db924ecfaf50088baf32f737d28027419ca5fee67c0c7141b29184e3", size = 134189733, upload-time = "2025-03-27T12:14:00.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b2/3205336262bf88d57f01503df81ede2a0b1eecbb2a7d58978a5e5625f7c1/PySide6_Essentials-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fd57fa0c886ef99b3844173322c0023ec77cc946a0c9a0cdfbc2ac5c511053c1", size = 95088226, upload-time = "2025-03-27T12:14:25.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/e9/c5c48563c0436465356dbef44ef0396713c1194c37c7bb4d75c83414b90c/PySide6_Essentials-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b4f4823f870b5bed477d6f7b6a3041839b859f70abfd703cf53208c73c2fe4cd", size = 92966444, upload-time = "2025-03-27T12:14:50.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/0f/5d8c6da7586e57ee032643e0c0e62335ef1a1add1a980160ddd1654f1d8d/PySide6_Essentials-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:3c0fae5550aff69f2166f46476c36e0ef56ce73d84829eac4559770b0c034b07", size = 72191029, upload-time = "2025-03-27T12:15:10.425Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qdrant-client"
|
||||
version = "1.16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "grpcio" },
|
||||
{ name = "httpx", extra = ["http2"] },
|
||||
{ name = "numpy" },
|
||||
{ name = "portalocker" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/68/fec3816a223c0b73b0e0036460be45c61ce2770ffb9197ac371e4f615ddc/qdrant_client-1.16.1.tar.gz", hash = "sha256:676c7c10fd4d4cb2981b8fcb32fd764f5f661b04b7334d024034d07212f971fd", size = 332130, upload-time = "2025-11-25T04:31:54.212Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/60/e2/60a20d04b0595c641516463168909c5bbcc192d3d6eacb637c1677109c6a/qdrant_client-1.16.1-py3-none-any.whl", hash = "sha256:1eefe89f66e8a468ba0de1680e28b441e69825cfb62e8fb2e457c15e24ce5e3b", size = 378481, upload-time = "2025-11-25T04:31:52.629Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semeion"
|
||||
version = "0.0.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "openai" },
|
||||
{ name = "pyside6" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "qdrant-client" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "openai", specifier = "~=2.8.1" },
|
||||
{ name = "pyside6", specifier = "~=6.8.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'" },
|
||||
{ name = "python-dotenv", specifier = "~=1.2.1" },
|
||||
{ name = "qdrant-client", specifier = "~=1.16.1" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "shiboken6"
|
||||
version = "6.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/89/162c763f2799786e16f9045aa599dcf126e0f5f6a517f1e0251ba0be17e6/shiboken6-6.8.3-cp39-abi3-macosx_12_0_universal2.whl", hash = "sha256:483efc7dd53c69147b8a8ade71f7619c79ffc683efcb1dc4f4cb6c40bb23d29b", size = 402402, upload-time = "2025-03-27T12:07:27.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/a1/f1958c9d00176044ab00464cd89b6969ef3a7d2ed12d316ff1eda3dec88f/shiboken6-6.8.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:295a003466ca2cccf6660e2f2ceb5e6cef4af192a48a196a32d46b6f0c9ec5cb", size = 204730, upload-time = "2025-03-27T12:07:29.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/8d/48ba0b33953592e54b688f2dceb06274f419f7743a3ea0f4e48faf837652/shiboken6-6.8.3-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:2b1a41348102952d2a5fbf3630bddd4d44112e18058b5e4cf505e51f2812429d", size = 201025, upload-time = "2025-03-27T12:07:31.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/29/e77f3fba5337f2d98f977eece0fd24cda6cf7d83be2699514d2ca1d34f6c/shiboken6-6.8.3-cp39-abi3-win_amd64.whl", hash = "sha256:bca3a94513ce9242f7d4bbdca902072a1631888e0aa3a8711a52cc5dbe93588f", size = 1150998, upload-time = "2025-03-27T12:07:33.38Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user