refactor: reorganize folder structure for better maintainability

Changes:
- Move migrations/ under open_notebook/database/migrations/
- Extract AI models to open_notebook/ai/ (Model, ModelManager, provision)
- Extract podcasts to open_notebook/podcasts/ (EpisodeProfile, SpeakerProfile, PodcastEpisode)
- Reorganize prompts to mirror graphs structure (chat/, source_chat/)

This improves code organization by:
- Consolidating database concerns (migrations now with database code)
- Separating AI infrastructure from domain entities
- Isolating podcast feature into its own module
- Creating consistent prompt/graph naming conventions

All 52 tests pass.
This commit is contained in:
LUIS NOVO 2026-01-03 14:04:27 -03:00
parent 93cda6c42a
commit ab5560c9a2
48 changed files with 50 additions and 47 deletions

View file

@ -7,7 +7,7 @@ from typing import List
from loguru import logger
from api.client import api_client
from open_notebook.domain.podcast import EpisodeProfile
from open_notebook.podcasts.models import EpisodeProfile
class EpisodeProfilesService:

View file

@ -1,5 +1,6 @@
# Load environment variables
from dotenv import load_dotenv
load_dotenv()
from contextlib import asynccontextmanager

View file

@ -7,7 +7,7 @@ from typing import List, Optional
from loguru import logger
from api.client import api_client
from open_notebook.domain.models import DefaultModels, Model
from open_notebook.ai.models import DefaultModels, Model
class ModelsService:

View file

@ -6,7 +6,7 @@ from pydantic import BaseModel
from surreal_commands import get_command_status, submit_command
from open_notebook.domain.notebook import Notebook
from open_notebook.domain.podcast import EpisodeProfile, PodcastEpisode, SpeakerProfile
from open_notebook.podcasts.models import EpisodeProfile, PodcastEpisode, SpeakerProfile
class PodcastGenerationRequest(BaseModel):

View file

@ -3,7 +3,7 @@ from loguru import logger
from api.command_service import CommandService
from api.models import EmbedRequest, EmbedResponse
from open_notebook.domain.models import model_manager
from open_notebook.ai.models import model_manager
from open_notebook.domain.notebook import Note, Source
router = APIRouter()

View file

@ -4,7 +4,7 @@ from fastapi import APIRouter, HTTPException
from loguru import logger
from pydantic import BaseModel, Field
from open_notebook.domain.podcast import EpisodeProfile
from open_notebook.podcasts.models import EpisodeProfile
router = APIRouter()

View file

@ -11,7 +11,7 @@ from api.models import (
ModelResponse,
ProviderAvailabilityResponse,
)
from open_notebook.domain.models import DefaultModels, Model
from open_notebook.ai.models import DefaultModels, Model
from open_notebook.exceptions import InvalidInputError
router = APIRouter()

View file

@ -6,7 +6,7 @@ from fastapi.responses import StreamingResponse
from loguru import logger
from api.models import AskRequest, AskResponse, SearchRequest, SearchResponse
from open_notebook.domain.models import Model, model_manager
from open_notebook.ai.models import Model, model_manager
from open_notebook.domain.notebook import text_search, vector_search
from open_notebook.exceptions import DatabaseOperationError, InvalidInputError
from open_notebook.graphs.ask import graph as ask_graph

View file

@ -4,7 +4,7 @@ from fastapi import APIRouter, HTTPException
from loguru import logger
from pydantic import BaseModel, Field
from open_notebook.domain.podcast import SpeakerProfile
from open_notebook.podcasts.models import SpeakerProfile
router = APIRouter()

View file

@ -12,7 +12,7 @@ from api.models import (
TransformationResponse,
TransformationUpdate,
)
from open_notebook.domain.models import Model
from open_notebook.ai.models import Model
from open_notebook.domain.transformation import DefaultPrompts, Transformation
from open_notebook.exceptions import InvalidInputError
from open_notebook.graphs.transformation import graph as transformation_graph

View file

@ -5,8 +5,8 @@ from loguru import logger
from pydantic import BaseModel
from surreal_commands import CommandInput, CommandOutput, command, submit_command
from open_notebook.ai.models import model_manager
from open_notebook.database.repository import ensure_record_id, repo_query
from open_notebook.domain.models import model_manager
from open_notebook.domain.notebook import Note, Source, SourceInsight
from open_notebook.utils.text_utils import split_text

View file

@ -8,7 +8,7 @@ from surreal_commands import CommandInput, CommandOutput, command
from open_notebook.config import DATA_FOLDER
from open_notebook.database.repository import ensure_record_id, repo_query
from open_notebook.domain.podcast import EpisodeProfile, PodcastEpisode, SpeakerProfile
from open_notebook.podcasts.models import EpisodeProfile, PodcastEpisode, SpeakerProfile
try:
from podcast_creator import configure, create_podcast

View file

@ -0,0 +1,2 @@
# AI infrastructure module
# Contains model configuration, provisioning, and management

View file

@ -2,7 +2,7 @@ from esperanto import LanguageModel
from langchain_core.language_models.chat_models import BaseChatModel
from loguru import logger
from open_notebook.domain.models import model_manager
from open_notebook.ai.models import model_manager
from open_notebook.utils import token_count

View file

@ -96,26 +96,26 @@ class AsyncMigrationManager:
def __init__(self):
"""Initialize migration manager."""
self.up_migrations = [
AsyncMigration.from_file("migrations/1.surrealql"),
AsyncMigration.from_file("migrations/2.surrealql"),
AsyncMigration.from_file("migrations/3.surrealql"),
AsyncMigration.from_file("migrations/4.surrealql"),
AsyncMigration.from_file("migrations/5.surrealql"),
AsyncMigration.from_file("migrations/6.surrealql"),
AsyncMigration.from_file("migrations/7.surrealql"),
AsyncMigration.from_file("migrations/8.surrealql"),
AsyncMigration.from_file("migrations/9.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/1.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/2.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/3.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/4.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/5.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/6.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/7.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/8.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/9.surrealql"),
]
self.down_migrations = [
AsyncMigration.from_file("migrations/1_down.surrealql"),
AsyncMigration.from_file("migrations/2_down.surrealql"),
AsyncMigration.from_file("migrations/3_down.surrealql"),
AsyncMigration.from_file("migrations/4_down.surrealql"),
AsyncMigration.from_file("migrations/5_down.surrealql"),
AsyncMigration.from_file("migrations/6_down.surrealql"),
AsyncMigration.from_file("migrations/7_down.surrealql"),
AsyncMigration.from_file("migrations/8_down.surrealql"),
AsyncMigration.from_file("migrations/9_down.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/1_down.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/2_down.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/3_down.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/4_down.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/5_down.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/6_down.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/7_down.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/8_down.surrealql"),
AsyncMigration.from_file("open_notebook/database/migrations/9_down.surrealql"),
]
self.runner = AsyncMigrationRunner(
up_migrations=self.up_migrations,

View file

@ -111,7 +111,7 @@ class ObjectModel(BaseModel):
return None
async def save(self) -> None:
from open_notebook.domain.models import model_manager
from open_notebook.ai.models import model_manager
try:
self.model_validate(self.model_dump(), strict=True)

View file

@ -6,9 +6,9 @@ from pydantic import BaseModel, Field, field_validator
from surreal_commands import submit_command
from surrealdb import RecordID
from open_notebook.ai.models import model_manager
from open_notebook.database.repository import ensure_record_id, repo_query
from open_notebook.domain.base import ObjectModel
from open_notebook.domain.models import model_manager
from open_notebook.exceptions import DatabaseOperationError, InvalidInputError
from open_notebook.utils import split_text

View file

@ -9,8 +9,8 @@ from langgraph.types import Send
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
from open_notebook.ai.provision import provision_langchain_model
from open_notebook.domain.notebook import vector_search
from open_notebook.graphs.utils import provision_langchain_model
from open_notebook.utils import clean_thinking_content

View file

@ -5,16 +5,15 @@ from typing import Annotated, Optional
from ai_prompter import Prompter
from langchain_core.messages import AIMessage, SystemMessage
from langchain_core.runnables import RunnableConfig
from open_notebook.utils import clean_thinking_content
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict
from open_notebook.ai.provision import provision_langchain_model
from open_notebook.config import LANGGRAPH_CHECKPOINT_FILE
from open_notebook.domain.notebook import Notebook
from open_notebook.graphs.utils import provision_langchain_model
from open_notebook.utils import clean_thinking_content
class ThreadState(TypedDict):
@ -26,7 +25,7 @@ class ThreadState(TypedDict):
def call_model_with_messages(state: ThreadState, config: RunnableConfig) -> dict:
system_prompt = Prompter(prompt_template="chat").render(data=state) # type: ignore[arg-type]
system_prompt = Prompter(prompt_template="chat/system").render(data=state) # type: ignore[arg-type]
payload = [SystemMessage(content=system_prompt)] + state.get("messages", [])
model_id = config.get("configurable", {}).get("model_id") or state.get(
"model_override"

View file

@ -6,7 +6,7 @@ from langchain_core.runnables import RunnableConfig
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
from open_notebook.graphs.utils import provision_langchain_model
from open_notebook.ai.provision import provision_langchain_model
class PatternChainState(TypedDict):

View file

@ -9,8 +9,8 @@ from langgraph.types import Send
from loguru import logger
from typing_extensions import Annotated, TypedDict
from open_notebook.ai.models import Model, ModelManager
from open_notebook.domain.content_settings import ContentSettings
from open_notebook.domain.models import Model, ModelManager
from open_notebook.domain.notebook import Asset, Source
from open_notebook.domain.transformation import Transformation
from open_notebook.graphs.transformation import graph as transform_graph

View file

@ -5,16 +5,15 @@ from typing import Annotated, Dict, List, Optional
from ai_prompter import Prompter
from langchain_core.messages import AIMessage, SystemMessage
from langchain_core.runnables import RunnableConfig
from open_notebook.utils import clean_thinking_content
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict
from open_notebook.ai.provision import provision_langchain_model
from open_notebook.config import LANGGRAPH_CHECKPOINT_FILE
from open_notebook.domain.notebook import Source, SourceInsight
from open_notebook.graphs.utils import provision_langchain_model
from open_notebook.utils import clean_thinking_content
from open_notebook.utils.context_builder import ContextBuilder
@ -111,7 +110,7 @@ def call_model_with_source_context(
}
# Apply the source_chat prompt template
system_prompt = Prompter(prompt_template="source_chat").render(data=prompt_data)
system_prompt = Prompter(prompt_template="source_chat/system").render(data=prompt_data)
payload = [SystemMessage(content=system_prompt)] + state.get("messages", [])
# Handle async model provisioning from sync context

View file

@ -4,9 +4,9 @@ from langchain_core.runnables import RunnableConfig
from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict
from open_notebook.ai.provision import provision_langchain_model
from open_notebook.domain.notebook import Source
from open_notebook.domain.transformation import DefaultPrompts, Transformation
from open_notebook.graphs.utils import provision_langchain_model
from open_notebook.utils import clean_thinking_content

View file

@ -0,0 +1,2 @@
# Podcasts module
# Contains podcast episode models, profiles, and generation logic

View file

@ -8,13 +8,13 @@ that can be tested without database mocking.
import pytest
from pydantic import ValidationError
from open_notebook.ai.models import ModelManager
from open_notebook.domain.base import RecordModel
from open_notebook.domain.content_settings import ContentSettings
from open_notebook.domain.models import ModelManager
from open_notebook.domain.notebook import Note, Notebook, Source
from open_notebook.domain.podcast import EpisodeProfile, SpeakerProfile
from open_notebook.domain.transformation import Transformation
from open_notebook.exceptions import InvalidInputError
from open_notebook.podcasts.models import EpisodeProfile, SpeakerProfile
# ============================================================================
# TEST SUITE 1: RecordModel Singleton Pattern

View file

@ -60,7 +60,7 @@ class TestModelCreation:
@patch("open_notebook.database.repository.repo_query")
async def test_create_same_model_name_different_provider(self, mock_repo_query, client):
"""Test that creating a model with same name but different provider is allowed."""
from open_notebook.domain.models import Model
from open_notebook.ai.models import Model
# Mock repo_query to return empty (no duplicate found for different provider)
mock_repo_query.return_value = []