demon-cry 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- demon_cry/__init__.py +0 -0
- demon_cry/__main__.py +25 -0
- demon_cry/alembic/env.py +54 -0
- demon_cry/alembic/script.py.mako +26 -0
- demon_cry/alembic/versions/.gitkeep +1 -0
- demon_cry/alembic/versions/61f19b2c5948_add_module_configs.py +37 -0
- demon_cry/alembic/versions/9f180422303d_create_users_table.py +36 -0
- demon_cry/alembic/versions/dbbc1fe7c7c2_add_llm_models_table.py +38 -0
- demon_cry/alembic/versions/e923be991bb7_create_settings_table.py +32 -0
- demon_cry/alembic.ini +36 -0
- demon_cry/api/__init__.py +16 -0
- demon_cry/api/admin/__init__.py +15 -0
- demon_cry/api/admin/llm_models.py +43 -0
- demon_cry/api/admin/modules.py +25 -0
- demon_cry/api/admin/settings.py +27 -0
- demon_cry/api/admin/users.py +38 -0
- demon_cry/api/dependencies/__init__.py +9 -0
- demon_cry/api/dependencies/auth.py +29 -0
- demon_cry/api/dependencies/config.py +9 -0
- demon_cry/api/dependencies/database.py +26 -0
- demon_cry/api/health.py +7 -0
- demon_cry/api/investigate.py +73 -0
- demon_cry/api/schemas/llm_models.py +24 -0
- demon_cry/api/schemas/modules.py +13 -0
- demon_cry/api/schemas/settings.py +10 -0
- demon_cry/api/schemas/users.py +14 -0
- demon_cry/api/tools.py +32 -0
- demon_cry/cli.py +136 -0
- demon_cry/core/config.py +10 -0
- demon_cry/core/module_registry.py +84 -0
- demon_cry/database/engine.py +24 -0
- demon_cry/database/models/__init__.py +9 -0
- demon_cry/database/models/base.py +5 -0
- demon_cry/database/models/llm_models.py +14 -0
- demon_cry/database/models/modules.py +13 -0
- demon_cry/database/models/settings.py +12 -0
- demon_cry/database/models/users.py +13 -0
- demon_cry/database/repositories/__init__.py +9 -0
- demon_cry/database/repositories/base.py +44 -0
- demon_cry/database/repositories/llm_models.py +9 -0
- demon_cry/database/repositories/modules.py +9 -0
- demon_cry/database/repositories/settings.py +9 -0
- demon_cry/database/repositories/users.py +9 -0
- demon_cry/services/llm.py +228 -0
- demon_cry/utils/version.py +8 -0
- demon_cry-0.8.0.dist-info/METADATA +23 -0
- demon_cry-0.8.0.dist-info/RECORD +50 -0
- demon_cry-0.8.0.dist-info/WHEEL +4 -0
- demon_cry-0.8.0.dist-info/entry_points.txt +3 -0
- demon_cry-0.8.0.dist-info/licenses/LICENSE +373 -0
demon_cry/__init__.py
ADDED
|
File without changes
|
demon_cry/__main__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
|
|
4
|
+
from fastapi import FastAPI
|
|
5
|
+
|
|
6
|
+
from demon_cry.utils import version
|
|
7
|
+
from demon_cry.core.module_registry import ModuleRegistry
|
|
8
|
+
from demon_cry.api import router
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
@asynccontextmanager
|
|
13
|
+
async def lifespan(app: FastAPI):
|
|
14
|
+
modules = ModuleRegistry()
|
|
15
|
+
await modules.discover()
|
|
16
|
+
app.state.registry = modules
|
|
17
|
+
yield
|
|
18
|
+
|
|
19
|
+
app = FastAPI(
|
|
20
|
+
title="demon cry core",
|
|
21
|
+
version=version.get_version(),
|
|
22
|
+
lifespan=lifespan
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
app.include_router(router)
|
demon_cry/alembic/env.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from logging.config import fileConfig
|
|
3
|
+
|
|
4
|
+
from alembic import context
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
6
|
+
|
|
7
|
+
from demon_cry.database.models.base import BaseModel as Base
|
|
8
|
+
from demon_cry.database.engine import engine
|
|
9
|
+
|
|
10
|
+
config = context.config
|
|
11
|
+
|
|
12
|
+
if config.config_file_name is not None:
|
|
13
|
+
try:
|
|
14
|
+
fileConfig(config.config_file_name)
|
|
15
|
+
except Exception:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
target_metadata = Base.metadata
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_migrations_offline() -> None:
|
|
22
|
+
url = str(engine.url)
|
|
23
|
+
context.configure(
|
|
24
|
+
url=url,
|
|
25
|
+
target_metadata=target_metadata,
|
|
26
|
+
literal_binds=True,
|
|
27
|
+
dialect_opts={"paramstyle": "named"},
|
|
28
|
+
render_as_batch=True,
|
|
29
|
+
)
|
|
30
|
+
with context.begin_transaction():
|
|
31
|
+
context.run_migrations()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def do_run_migrations(connection) -> None:
|
|
35
|
+
context.configure(
|
|
36
|
+
connection=connection,
|
|
37
|
+
target_metadata=target_metadata,
|
|
38
|
+
render_as_batch=True,
|
|
39
|
+
)
|
|
40
|
+
with context.begin_transaction():
|
|
41
|
+
context.run_migrations()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def run_migrations_online() -> None:
|
|
45
|
+
connectable: AsyncEngine = engine
|
|
46
|
+
async with connectable.connect() as connection:
|
|
47
|
+
await connection.run_sync(do_run_migrations)
|
|
48
|
+
await connectable.dispose()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if context.is_offline_mode():
|
|
52
|
+
run_migrations_offline()
|
|
53
|
+
else:
|
|
54
|
+
asyncio.run(run_migrations_online())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
${imports if imports else ""}
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = ${repr(up_revision)}
|
|
16
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
${upgrades if upgrades else "pass"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def downgrade() -> None:
|
|
26
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Keep this directory in the package so it ships in the wheel.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""add module configs
|
|
2
|
+
|
|
3
|
+
Revision ID: 61f19b2c5948
|
|
4
|
+
Revises: 9f180422303d
|
|
5
|
+
Create Date: 2026-09-06 00:27:25.582224
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = '61f19b2c5948'
|
|
16
|
+
down_revision: Union[str, None] = '9f180422303d'
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
23
|
+
op.create_table('module',
|
|
24
|
+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
25
|
+
sa.Column('module_name', sa.String(length=64), nullable=False),
|
|
26
|
+
sa.Column('config', sa.JSON(), nullable=False),
|
|
27
|
+
sa.Column('enabled', sa.Boolean(), nullable=False),
|
|
28
|
+
sa.PrimaryKeyConstraint('id'),
|
|
29
|
+
sa.UniqueConstraint('module_name')
|
|
30
|
+
)
|
|
31
|
+
# ### end Alembic commands ###
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def downgrade() -> None:
|
|
35
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
36
|
+
op.drop_table('module')
|
|
37
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""create_users_table
|
|
2
|
+
|
|
3
|
+
Revision ID: 9f180422303d
|
|
4
|
+
Revises: e923be991bb7
|
|
5
|
+
Create Date: 2026-09-01 12:29:47.644009
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = '9f180422303d'
|
|
16
|
+
down_revision: Union[str, None] = 'e923be991bb7'
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
23
|
+
op.create_table('users',
|
|
24
|
+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
25
|
+
sa.Column('username', sa.String(length=32), nullable=False),
|
|
26
|
+
sa.Column('credentials', sa.String(length=256), nullable=False),
|
|
27
|
+
sa.Column('is_admin', sa.Boolean(), nullable=False),
|
|
28
|
+
sa.PrimaryKeyConstraint('id')
|
|
29
|
+
)
|
|
30
|
+
# ### end Alembic commands ###
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def downgrade() -> None:
|
|
34
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
35
|
+
op.drop_table('users')
|
|
36
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""add llm_models table
|
|
2
|
+
|
|
3
|
+
Revision ID: dbbc1fe7c7c2
|
|
4
|
+
Revises: 61f19b2c5948
|
|
5
|
+
Create Date: 2026-09-07 22:39:41.032888
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = 'dbbc1fe7c7c2'
|
|
16
|
+
down_revision: Union[str, None] = '61f19b2c5948'
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
23
|
+
op.create_table('llm_models',
|
|
24
|
+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
25
|
+
sa.Column('base_url', sa.String(), nullable=False),
|
|
26
|
+
sa.Column('api_key', sa.String(), nullable=False),
|
|
27
|
+
sa.Column('model_name', sa.String(length=64), nullable=False),
|
|
28
|
+
sa.Column('is_default', sa.Boolean(), nullable=False),
|
|
29
|
+
sa.PrimaryKeyConstraint('id'),
|
|
30
|
+
sa.UniqueConstraint('model_name')
|
|
31
|
+
)
|
|
32
|
+
# ### end Alembic commands ###
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def downgrade() -> None:
|
|
36
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
37
|
+
op.drop_table('llm_models')
|
|
38
|
+
# ### end Alembic commands ###
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""create settings table
|
|
2
|
+
|
|
3
|
+
Revision ID: e923be991bb7
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2026-08-30 16:35:07.058549
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from typing import Sequence, Union
|
|
9
|
+
|
|
10
|
+
from alembic import op
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = 'e923be991bb7'
|
|
16
|
+
down_revision: Union[str, None] = None
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
op.create_table(
|
|
23
|
+
"kv_settings",
|
|
24
|
+
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|
25
|
+
sa.Column("key", sa.String(32), nullable=False),
|
|
26
|
+
sa.Column("value", sa.Text(), nullable=False),
|
|
27
|
+
sa.PrimaryKeyConstraint("id"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def downgrade() -> None:
|
|
32
|
+
op.drop_table("kv_settings")
|
demon_cry/alembic.ini
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[alembic]
|
|
2
|
+
script_location = %(here)s/alembic
|
|
3
|
+
|
|
4
|
+
[loggers]
|
|
5
|
+
keys = root,sqlalchemy,alembic
|
|
6
|
+
|
|
7
|
+
[handlers]
|
|
8
|
+
keys = console
|
|
9
|
+
|
|
10
|
+
[formatters]
|
|
11
|
+
keys = generic
|
|
12
|
+
|
|
13
|
+
[logger_root]
|
|
14
|
+
level = WARNING
|
|
15
|
+
handlers = console
|
|
16
|
+
qualname =
|
|
17
|
+
|
|
18
|
+
[logger_sqlalchemy]
|
|
19
|
+
level = WARNING
|
|
20
|
+
handlers =
|
|
21
|
+
qualname = sqlalchemy.engine
|
|
22
|
+
|
|
23
|
+
[logger_alembic]
|
|
24
|
+
level = INFO
|
|
25
|
+
handlers =
|
|
26
|
+
qualname = alembic
|
|
27
|
+
|
|
28
|
+
[handler_console]
|
|
29
|
+
class = StreamHandler
|
|
30
|
+
args = (sys.stderr,)
|
|
31
|
+
level = NOTSET
|
|
32
|
+
formatter = generic
|
|
33
|
+
|
|
34
|
+
[formatter_generic]
|
|
35
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
36
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from demon_cry.api import (
|
|
4
|
+
investigate,
|
|
5
|
+
health,
|
|
6
|
+
tools
|
|
7
|
+
)
|
|
8
|
+
from demon_cry.api.admin import admin_router
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/api")
|
|
11
|
+
|
|
12
|
+
router.include_router(health.router, tags=["System"])
|
|
13
|
+
router.include_router(investigate.router, tags=["Investigation"])
|
|
14
|
+
router.include_router(tools.router, tags=["Tools"])
|
|
15
|
+
|
|
16
|
+
router.include_router(admin_router)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends
|
|
2
|
+
from demon_cry.api.dependencies.auth import require_admin
|
|
3
|
+
|
|
4
|
+
from demon_cry.api.admin.users import users_router
|
|
5
|
+
from demon_cry.api.admin.settings import settings_router
|
|
6
|
+
from demon_cry.api.admin.modules import modules_router
|
|
7
|
+
from demon_cry.api.admin.llm_models import llm_models_router
|
|
8
|
+
|
|
9
|
+
admin_router = APIRouter(prefix="/admin", dependencies=[Depends(require_admin)])
|
|
10
|
+
admin_router.include_router(users_router, tags=["Users"])
|
|
11
|
+
admin_router.include_router(settings_router, tags=["Settings"])
|
|
12
|
+
admin_router.include_router(modules_router, tags=["Modules"])
|
|
13
|
+
admin_router.include_router(llm_models_router, tags=["LLM Models"])
|
|
14
|
+
|
|
15
|
+
__all__ = ["admin_router"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
|
|
3
|
+
from demon_cry.api.dependencies import LLMRepo
|
|
4
|
+
from demon_cry.api.schemas.llm_models import LLMModelResponse, LLMModelCreate, LLMModelUpdate
|
|
5
|
+
|
|
6
|
+
llm_models_router = APIRouter(prefix="/llm-models")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@llm_models_router.get("/", response_model=list[LLMModelResponse])
|
|
10
|
+
async def get_all_models(llm_repo: LLMRepo):
|
|
11
|
+
return await llm_repo.get_all()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@llm_models_router.get("/{model_name}", response_model=LLMModelResponse)
|
|
15
|
+
async def get_model(model_name: str, llm_repo: LLMRepo):
|
|
16
|
+
model = await llm_repo.get(model_name=model_name)
|
|
17
|
+
if not model:
|
|
18
|
+
raise HTTPException(status_code=404, detail="LLM model not found")
|
|
19
|
+
return model
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@llm_models_router.post("/", response_model=LLMModelResponse, status_code=201)
|
|
23
|
+
async def create_model(body: LLMModelCreate, llm_repo: LLMRepo):
|
|
24
|
+
existing = await llm_repo.get(model_name=body.model_name)
|
|
25
|
+
if existing:
|
|
26
|
+
raise HTTPException(status_code=409, detail="Model with this name already exists")
|
|
27
|
+
return await llm_repo.create(**body.model_dump())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@llm_models_router.patch("/{model_name}", response_model=LLMModelResponse)
|
|
31
|
+
async def update_model(model_name: str, body: LLMModelUpdate, llm_repo: LLMRepo):
|
|
32
|
+
model = await llm_repo.get(model_name=model_name)
|
|
33
|
+
if not model:
|
|
34
|
+
raise HTTPException(status_code=404, detail="LLM model not found")
|
|
35
|
+
return await llm_repo.update(model, **body.model_dump(exclude_unset=True))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@llm_models_router.delete("/{model_name}", status_code=204)
|
|
39
|
+
async def delete_model(model_name: str, llm_repo: LLMRepo):
|
|
40
|
+
model = await llm_repo.get(model_name=model_name)
|
|
41
|
+
if not model:
|
|
42
|
+
raise HTTPException(status_code=404, detail="LLM model not found")
|
|
43
|
+
await llm_repo.delete(model)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
|
|
3
|
+
from demon_cry.api.dependencies import ModuleRepo
|
|
4
|
+
from demon_cry.api.schemas.modules import ModuleResponse, ModuleUpdate
|
|
5
|
+
|
|
6
|
+
modules_router = APIRouter(prefix="/modules")
|
|
7
|
+
|
|
8
|
+
@modules_router.get("/", response_model=list[ModuleResponse])
|
|
9
|
+
async def get_all_modules(module_repo: ModuleRepo):
|
|
10
|
+
modules = await module_repo.get_all()
|
|
11
|
+
return modules
|
|
12
|
+
|
|
13
|
+
@modules_router.get("/{module_name}", response_model=ModuleResponse)
|
|
14
|
+
async def get_module(module_name: str, module_repo: ModuleRepo):
|
|
15
|
+
module = await module_repo.get(module_name=module_name)
|
|
16
|
+
if not module:
|
|
17
|
+
raise HTTPException(status_code=404, detail="Module not found")
|
|
18
|
+
return module
|
|
19
|
+
|
|
20
|
+
@modules_router.patch("/{module_name}", response_model=ModuleResponse)
|
|
21
|
+
async def update_module(module_name: str, body: ModuleUpdate, module_repo: ModuleRepo):
|
|
22
|
+
module = await module_repo.get(module_name=module_name)
|
|
23
|
+
if not module:
|
|
24
|
+
raise HTTPException(status_code=404, detail="Module not found")
|
|
25
|
+
return await module_repo.update(module, **body.model_dump(exclude_unset=True))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
|
|
3
|
+
from demon_cry.api.dependencies import SettingsRepo
|
|
4
|
+
from demon_cry.api.schemas.settings import SettingResponse, SettingUpdate
|
|
5
|
+
|
|
6
|
+
settings_router = APIRouter(prefix="/settings")
|
|
7
|
+
|
|
8
|
+
@settings_router.get("/", response_model=list[SettingResponse])
|
|
9
|
+
async def get_all_settings(set_repo: SettingsRepo):
|
|
10
|
+
settings = await set_repo.get_all()
|
|
11
|
+
if not settings:
|
|
12
|
+
raise HTTPException(status_code=404, detail="Settings not found")
|
|
13
|
+
return settings
|
|
14
|
+
|
|
15
|
+
@settings_router.get("/{key}", response_model=SettingResponse)
|
|
16
|
+
async def get_settings(key: str, set_repo: SettingsRepo):
|
|
17
|
+
settings = await set_repo.get(key=key)
|
|
18
|
+
if not settings:
|
|
19
|
+
raise HTTPException(status_code=404, detail="Settings not found")
|
|
20
|
+
return settings
|
|
21
|
+
|
|
22
|
+
@settings_router.patch("/{key}", response_model=SettingResponse)
|
|
23
|
+
async def update_settings(key: str, body: SettingUpdate, set_repo: SettingsRepo):
|
|
24
|
+
settings = await set_repo.get(key=key)
|
|
25
|
+
if not settings:
|
|
26
|
+
raise HTTPException(status_code=404, detail="Settings not found")
|
|
27
|
+
return await set_repo.update(settings, **body.model_dump(exclude_unset=True))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
from fastapi import APIRouter, HTTPException
|
|
3
|
+
|
|
4
|
+
from demon_cry.api.dependencies import UserRepo
|
|
5
|
+
from demon_cry.api.schemas.users import UserResponse, UserCreate, UserUpdate
|
|
6
|
+
|
|
7
|
+
users_router = APIRouter(prefix="/users")
|
|
8
|
+
|
|
9
|
+
@users_router.post(path="/", status_code=201, response_model=UserResponse)
|
|
10
|
+
async def create_user(body: UserCreate, user_repo: UserRepo):
|
|
11
|
+
token = secrets.token_urlsafe(32)
|
|
12
|
+
|
|
13
|
+
user = await user_repo.create(
|
|
14
|
+
username=body.username,
|
|
15
|
+
credentials=token
|
|
16
|
+
)
|
|
17
|
+
return user
|
|
18
|
+
|
|
19
|
+
@users_router.get(path="/{user_id}", response_model=UserResponse)
|
|
20
|
+
async def get_user(user_id: int, user_repo: UserRepo):
|
|
21
|
+
user = await user_repo.get(id=user_id)
|
|
22
|
+
if not user:
|
|
23
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
24
|
+
return user
|
|
25
|
+
|
|
26
|
+
@users_router.patch(path="/{user_id}", response_model=UserResponse)
|
|
27
|
+
async def update_user(user_id: int, body: UserUpdate, user_repo: UserRepo):
|
|
28
|
+
user = await user_repo.get(id=user_id)
|
|
29
|
+
if not user:
|
|
30
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
31
|
+
return await user_repo.update(user, **body.model_dump(exclude_unset=True))
|
|
32
|
+
|
|
33
|
+
@users_router.delete(path="/{user_id}", status_code=204)
|
|
34
|
+
async def delete_user(user_id: int, user_repo: UserRepo):
|
|
35
|
+
user = await user_repo.get(id=user_id)
|
|
36
|
+
if not user:
|
|
37
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
38
|
+
await user_repo.delete(user)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from demon_cry.api.dependencies.auth import CurrentUser
|
|
2
|
+
from demon_cry.api.dependencies.database import UserRepo, SettingsRepo, ModuleRepo, LLMRepo
|
|
3
|
+
from demon_cry.api.dependencies.config import AppRegistry
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"CurrentUser",
|
|
7
|
+
"UserRepo", "SettingsRepo", "ModuleRepo", "LLMRepo",
|
|
8
|
+
"AppRegistry"
|
|
9
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from fastapi import Depends, HTTPException, Security
|
|
3
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
4
|
+
|
|
5
|
+
from demon_cry.database.models.users import UserModel
|
|
6
|
+
from demon_cry.api.dependencies.database import UserRepo
|
|
7
|
+
|
|
8
|
+
security = HTTPBearer(auto_error=False)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def get_current_user(
|
|
12
|
+
user_repo: UserRepo,
|
|
13
|
+
credentials: HTTPAuthorizationCredentials = Security(security),
|
|
14
|
+
) -> UserModel:
|
|
15
|
+
if not credentials:
|
|
16
|
+
raise HTTPException(status_code=401, detail="Missing API key")
|
|
17
|
+
|
|
18
|
+
user = await user_repo.get(credentials=credentials.credentials)
|
|
19
|
+
if not user:
|
|
20
|
+
raise HTTPException(status_code=401, detail="Invalid API key")
|
|
21
|
+
|
|
22
|
+
return user
|
|
23
|
+
|
|
24
|
+
CurrentUser = Annotated[UserModel, Depends(get_current_user)]
|
|
25
|
+
|
|
26
|
+
async def require_admin(user: CurrentUser) -> UserModel:
|
|
27
|
+
if not user.is_admin:
|
|
28
|
+
raise HTTPException(status_code=403, detail="Admin access required")
|
|
29
|
+
return user
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from fastapi import Depends, Request
|
|
3
|
+
|
|
4
|
+
from demon_cry.core.module_registry import ModuleRegistry
|
|
5
|
+
|
|
6
|
+
def get_registry(request: Request) -> ModuleRegistry:
|
|
7
|
+
return request.app.state.registry
|
|
8
|
+
|
|
9
|
+
AppRegistry = Annotated[ModuleRegistry, Depends(get_registry)]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from fastapi import Depends
|
|
3
|
+
|
|
4
|
+
from demon_cry.database.engine import get_session
|
|
5
|
+
|
|
6
|
+
from demon_cry.database.repositories import SettingsRepository, UserRepository, ModuleRepository, LLMRepository
|
|
7
|
+
|
|
8
|
+
def get_settings_repo(session=Depends(get_session)) -> SettingsRepository:
|
|
9
|
+
return SettingsRepository(session)
|
|
10
|
+
|
|
11
|
+
SettingsRepo = Annotated[SettingsRepository, Depends(get_settings_repo)]
|
|
12
|
+
|
|
13
|
+
def get_module_repo(session=Depends(get_session)) -> ModuleRepository:
|
|
14
|
+
return ModuleRepository(session)
|
|
15
|
+
|
|
16
|
+
ModuleRepo = Annotated[ModuleRepository, Depends(get_module_repo)]
|
|
17
|
+
|
|
18
|
+
def get_user_repo(session=Depends(get_session)) -> UserRepository:
|
|
19
|
+
return UserRepository(session)
|
|
20
|
+
|
|
21
|
+
UserRepo = Annotated[UserRepository, Depends(get_user_repo)]
|
|
22
|
+
|
|
23
|
+
def get_llm_repo(session=Depends(get_session)) -> LLMRepository:
|
|
24
|
+
return LLMRepository(session)
|
|
25
|
+
|
|
26
|
+
LLMRepo = Annotated[LLMRepository, Depends(get_llm_repo)]
|
demon_cry/api/health.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from demon_cry.api.dependencies import CurrentUser, LLMRepo, AppRegistry, SettingsRepo
|
|
7
|
+
from demon_cry.services.llm import LLM, TokenUsage, ToolUsage, DEFAULT_SYSTEM_PROMPT, DEFAULT_ITERATION_LIMIT
|
|
8
|
+
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
class OSINTRequest(BaseModel):
|
|
14
|
+
target: str
|
|
15
|
+
llm_model_id: int | None = None
|
|
16
|
+
|
|
17
|
+
class OSINTResponse(BaseModel):
|
|
18
|
+
status: str
|
|
19
|
+
result: str
|
|
20
|
+
tools_used: ToolUsage = ToolUsage()
|
|
21
|
+
tokens: TokenUsage = TokenUsage()
|
|
22
|
+
|
|
23
|
+
@router.post(path="/investigate")
|
|
24
|
+
async def investigate(
|
|
25
|
+
req: OSINTRequest,
|
|
26
|
+
registry: AppRegistry,
|
|
27
|
+
llm_repo: LLMRepo,
|
|
28
|
+
set_repo: SettingsRepo,
|
|
29
|
+
user: CurrentUser,
|
|
30
|
+
):
|
|
31
|
+
try:
|
|
32
|
+
if req.llm_model_id:
|
|
33
|
+
model = await llm_repo.get_by_id(req.llm_model_id)
|
|
34
|
+
else:
|
|
35
|
+
model = await llm_repo.get(is_default=True)
|
|
36
|
+
if not model:
|
|
37
|
+
raise HTTPException(status_code=404, detail="LLM model not found")
|
|
38
|
+
|
|
39
|
+
# Fetch settings from DB
|
|
40
|
+
system_prompt_row = await set_repo.get(key="system_prompt")
|
|
41
|
+
iteration_limit_row = await set_repo.get(key="iteration_limit")
|
|
42
|
+
|
|
43
|
+
system_prompt = system_prompt_row.value if system_prompt_row else DEFAULT_SYSTEM_PROMPT
|
|
44
|
+
iteration_limit = int(iteration_limit_row.value) if iteration_limit_row else DEFAULT_ITERATION_LIMIT
|
|
45
|
+
|
|
46
|
+
llm = LLM(
|
|
47
|
+
base_url=model.base_url,
|
|
48
|
+
api_key=model.api_key,
|
|
49
|
+
model=model.model_name,
|
|
50
|
+
registry=registry,
|
|
51
|
+
system_prompt=system_prompt,
|
|
52
|
+
iteration_limit=iteration_limit,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
res, tools, tokens = await llm.run_chain(user_query=req.target)
|
|
56
|
+
if res is None:
|
|
57
|
+
return OSINTResponse(
|
|
58
|
+
status="error",
|
|
59
|
+
result="Модель не завершила работу за отведённые итерации",
|
|
60
|
+
tools_used=tools,
|
|
61
|
+
tokens=tokens,
|
|
62
|
+
)
|
|
63
|
+
return OSINTResponse(
|
|
64
|
+
status="success",
|
|
65
|
+
result=res,
|
|
66
|
+
tools_used=tools,
|
|
67
|
+
tokens=tokens,
|
|
68
|
+
)
|
|
69
|
+
except HTTPException:
|
|
70
|
+
raise
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error("Investigation failed: %s", e)
|
|
73
|
+
return OSINTResponse(status="error", result="ошибка")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class LLMModelResponse(BaseModel):
|
|
5
|
+
id: int
|
|
6
|
+
model_name: str
|
|
7
|
+
base_url: str
|
|
8
|
+
is_default: bool
|
|
9
|
+
|
|
10
|
+
model_config = {"from_attributes": True}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LLMModelCreate(BaseModel):
|
|
14
|
+
model_name: str
|
|
15
|
+
base_url: str
|
|
16
|
+
api_key: str
|
|
17
|
+
is_default: bool = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LLMModelUpdate(BaseModel):
|
|
21
|
+
model_name: str | None = None
|
|
22
|
+
base_url: str | None = None
|
|
23
|
+
api_key: str | None = None
|
|
24
|
+
is_default: bool | None = None
|