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.
Files changed (50) hide show
  1. demon_cry/__init__.py +0 -0
  2. demon_cry/__main__.py +25 -0
  3. demon_cry/alembic/env.py +54 -0
  4. demon_cry/alembic/script.py.mako +26 -0
  5. demon_cry/alembic/versions/.gitkeep +1 -0
  6. demon_cry/alembic/versions/61f19b2c5948_add_module_configs.py +37 -0
  7. demon_cry/alembic/versions/9f180422303d_create_users_table.py +36 -0
  8. demon_cry/alembic/versions/dbbc1fe7c7c2_add_llm_models_table.py +38 -0
  9. demon_cry/alembic/versions/e923be991bb7_create_settings_table.py +32 -0
  10. demon_cry/alembic.ini +36 -0
  11. demon_cry/api/__init__.py +16 -0
  12. demon_cry/api/admin/__init__.py +15 -0
  13. demon_cry/api/admin/llm_models.py +43 -0
  14. demon_cry/api/admin/modules.py +25 -0
  15. demon_cry/api/admin/settings.py +27 -0
  16. demon_cry/api/admin/users.py +38 -0
  17. demon_cry/api/dependencies/__init__.py +9 -0
  18. demon_cry/api/dependencies/auth.py +29 -0
  19. demon_cry/api/dependencies/config.py +9 -0
  20. demon_cry/api/dependencies/database.py +26 -0
  21. demon_cry/api/health.py +7 -0
  22. demon_cry/api/investigate.py +73 -0
  23. demon_cry/api/schemas/llm_models.py +24 -0
  24. demon_cry/api/schemas/modules.py +13 -0
  25. demon_cry/api/schemas/settings.py +10 -0
  26. demon_cry/api/schemas/users.py +14 -0
  27. demon_cry/api/tools.py +32 -0
  28. demon_cry/cli.py +136 -0
  29. demon_cry/core/config.py +10 -0
  30. demon_cry/core/module_registry.py +84 -0
  31. demon_cry/database/engine.py +24 -0
  32. demon_cry/database/models/__init__.py +9 -0
  33. demon_cry/database/models/base.py +5 -0
  34. demon_cry/database/models/llm_models.py +14 -0
  35. demon_cry/database/models/modules.py +13 -0
  36. demon_cry/database/models/settings.py +12 -0
  37. demon_cry/database/models/users.py +13 -0
  38. demon_cry/database/repositories/__init__.py +9 -0
  39. demon_cry/database/repositories/base.py +44 -0
  40. demon_cry/database/repositories/llm_models.py +9 -0
  41. demon_cry/database/repositories/modules.py +9 -0
  42. demon_cry/database/repositories/settings.py +9 -0
  43. demon_cry/database/repositories/users.py +9 -0
  44. demon_cry/services/llm.py +228 -0
  45. demon_cry/utils/version.py +8 -0
  46. demon_cry-0.8.0.dist-info/METADATA +23 -0
  47. demon_cry-0.8.0.dist-info/RECORD +50 -0
  48. demon_cry-0.8.0.dist-info/WHEEL +4 -0
  49. demon_cry-0.8.0.dist-info/entry_points.txt +3 -0
  50. 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)
@@ -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)]
@@ -0,0 +1,7 @@
1
+ from fastapi import APIRouter
2
+
3
+ router = APIRouter()
4
+
5
+ @router.get("/health")
6
+ async def health():
7
+ return {"status": "ok"}
@@ -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
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel
2
+
3
+ class ModuleResponse(BaseModel):
4
+
5
+ id: int
6
+ module_name: str
7
+ config: dict
8
+ enabled: bool
9
+
10
+ class ModuleUpdate(BaseModel):
11
+
12
+ config: dict | None
13
+ enabled: bool | None
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel
2
+
3
+ class SettingResponse(BaseModel):
4
+
5
+ key: str
6
+ value: str
7
+
8
+ class SettingUpdate(BaseModel):
9
+
10
+ value: str