Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3c67761
feat(memory): add chat_memories table
NKTKLN Jun 16, 2026
56d102f
build(alembic): add chat_memories table
NKTKLN Jun 16, 2026
b71e31e
fix(memory): update tables relations (now one-to-one)
NKTKLN Jun 16, 2026
1332883
build(alembic): fix tables relations
NKTKLN Jun 16, 2026
26cf2c9
feat(memory): add chat memory repository
NKTKLN Jun 16, 2026
50648e3
feat(memory): integrate memory in sessions
NKTKLN Jun 16, 2026
b53790b
test(memory): fix tests
NKTKLN Jun 16, 2026
a971cc1
docs(chat): update docstring
NKTKLN Jun 16, 2026
b9e8522
feat(memory): add basic promts and summarizer
NKTKLN Jun 16, 2026
d2c4de0
fix(llm): isolate llm service from message model
NKTKLN Jun 17, 2026
40f8e77
refactor(memory): refactor summarizer prompt
NKTKLN Jun 17, 2026
a947497
feat(memory): add facts extractor service
NKTKLN Jun 17, 2026
1460449
feat(memory): add memory prompt builder
NKTKLN Jun 17, 2026
ca42ca0
style(llm): minor style fix
NKTKLN Jun 17, 2026
fb32e33
feat(memory): add chat memory schemas
NKTKLN Jun 17, 2026
2a3caaa
feat(memory): add chat memory exception
NKTKLN Jun 17, 2026
06acc83
feat(memory): add chat memory service
NKTKLN Jun 17, 2026
2310caf
style(job): update docstrinngs
NKTKLN Jun 17, 2026
66bc764
feat(memory): add chat memory dependency
NKTKLN Jun 17, 2026
3041a2e
feat(memory): add chat session memory endpoint
NKTKLN Jun 17, 2026
099cadf
test(llm): fix input vars in service class
NKTKLN Jun 17, 2026
80eeed4
test(memory): add memory repository tests
NKTKLN Jun 17, 2026
e638634
test(memory): add memory service tests
NKTKLN Jun 17, 2026
5bc4c92
test(memory): add memory module tests
NKTKLN Jun 17, 2026
341212c
fix(llm): add instructios in stream response
NKTKLN Jun 17, 2026
cb18a89
test(memory): add test for memory endpoint
NKTKLN Jun 17, 2026
0523578
feat(memory): add new column in chat_memories table
NKTKLN Jun 17, 2026
6d7b40b
build(memory): add new column in chat_memories table
NKTKLN Jun 17, 2026
cd00cdd
docs(tasks): update generation task docstring
NKTKLN Jun 17, 2026
2e3692c
style: remove keys in args
NKTKLN Jun 17, 2026
06bbc56
feat(memory): add new exception
NKTKLN Jun 17, 2026
33c69c3
feat(memory): add last_summarized_message_id column in chat_memories …
NKTKLN Jun 17, 2026
601314c
build(alembic): last_summarized_message_id column in chat_memories table
NKTKLN Jun 17, 2026
0e5020c
fix(memory): fix return value
NKTKLN Jun 17, 2026
b1b59be
refactor(messages): replace args with kwargs
NKTKLN Jun 17, 2026
89edf39
feat(message): add new function get_messages_after
NKTKLN Jun 17, 2026
aa03247
feat(memory): rewrite memory update fuction (connect with memory serv…
NKTKLN Jun 17, 2026
13ec20d
feat(workers): add update memory task
NKTKLN Jun 17, 2026
da2fef3
fix(celery): fix celery db connection
NKTKLN Jun 17, 2026
b2abf5b
fix(memory): fix dependency error
NKTKLN Jun 17, 2026
3c20636
test(memory): fix tests after memory service update & add new
NKTKLN Jun 18, 2026
8a16a74
docs(readme): update readme for new version
NKTKLN Jun 18, 2026
ae627fc
fix(mypy): fix typing
NKTKLN Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

**AI Notes API** is a production-oriented FastAPI backend for managing AI-related notes, prompts, conversations, and LLM workflows. The project demonstrates clean architecture, async development, PostgreSQL integration, and practical backend patterns for AI engineering.

The assistant is agentic: during chat completions it can call a built-in note toolkit (search, create, read, update, delete notes), and every chat session keeps a long-term memory built from extracted facts and rolling conversation summaries.

## 📦 Dependencies

* [Python 3.13+](https://www.python.org/downloads/)
Expand All @@ -13,7 +15,7 @@ Runtime services:

* [PostgreSQL](https://www.postgresql.org/) - primary data store
* [Redis](https://redis.io/) - Celery broker and result backend
* [Celery](https://docs.celeryq.dev/) - background worker for async LLM generation jobs
* [Celery](https://docs.celeryq.dev/) - background worker for async LLM generation jobs and chat memory updates

## 📌 API endpoints

Expand Down Expand Up @@ -41,6 +43,7 @@ Chat session endpoints (authenticated):
* `PATCH /api/v1/chat/sessions/{session_id}` - update a chat session by ID
* `DELETE /api/v1/chat/sessions/{session_id}` - delete a chat session by ID
* `GET /api/v1/chat/sessions/{session_id}/messages` - list messages in a session
* `GET /api/v1/chat/sessions/{session_id}/memory` - get the long-term memory for a chat session

Chat message endpoints (authenticated):

Expand Down
34 changes: 34 additions & 0 deletions alembic/versions/3e83b1260f95_fix_tables_relations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""'Fix tables relations'

Revision ID: 3e83b1260f95
Revises: 45387e4ce52c
Create Date: 2026-06-17 01:25:22.088598

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '3e83b1260f95'
down_revision: Union[str, Sequence[str], None] = '45387e4ce52c'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_chat_memories_session_id'), table_name='chat_memories')
op.create_index(op.f('ix_chat_memories_session_id'), 'chat_memories', ['session_id'], unique=True)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_chat_memories_session_id'), table_name='chat_memories')
op.create_index(op.f('ix_chat_memories_session_id'), 'chat_memories', ['session_id'], unique=False)
# ### end Alembic commands ###
43 changes: 43 additions & 0 deletions alembic/versions/45387e4ce52c_create_chat_memories_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""'Create chat_memories table'

Revision ID: 45387e4ce52c
Revises: a6ca0638c302
Create Date: 2026-06-17 01:22:47.022259

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = '45387e4ce52c'
down_revision: Union[str, Sequence[str], None] = 'a6ca0638c302'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('chat_memories',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('session_id', sa.Uuid(), nullable=False),
sa.Column('summary', sa.Text(), nullable=False),
sa.Column('facts', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['session_id'], ['chat_sessions.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_chat_memories_session_id'), 'chat_memories', ['session_id'], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_chat_memories_session_id'), table_name='chat_memories')
op.drop_table('chat_memories')
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""'Add is_summarizing column in chat_memories table'

Revision ID: 791080acc188
Revises: 3e83b1260f95
Create Date: 2026-06-18 01:02:05.405819

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '791080acc188'
down_revision: Union[str, Sequence[str], None] = '3e83b1260f95'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""'Add last_summarized_message_id column in chat_memories table'

Revision ID: 93228745b965
Revises: 791080acc188
Create Date: 2026-06-18 01:52:11.606934

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '93228745b965'
down_revision: Union[str, Sequence[str], None] = '791080acc188'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('chat_memories', sa.Column('last_summarized_message_id', sa.Uuid(), nullable=True))
op.create_index(op.f('ix_chat_memories_last_summarized_message_id'), 'chat_memories', ['last_summarized_message_id'], unique=False)
op.create_foreign_key(None, 'chat_memories', 'messages', ['last_summarized_message_id'], ['id'], ondelete='SET NULL')
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'chat_memories', type_='foreignkey')
op.drop_index(op.f('ix_chat_memories_last_summarized_message_id'), table_name='chat_memories')
op.drop_column('chat_memories', 'last_summarized_message_id')
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ai_notes_api"
version = "0.3.0"
version = "0.4.0"
description = "AI Note's API"
authors = [{ name = "NKTKLN", email = "nktkln@nktkln.com" }]
readme = "README.md"
Expand Down
44 changes: 43 additions & 1 deletion src/ai_notes_api/api/v1/chat_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
from ai_notes_api.api.v1.dependencies import (
get_chat_session_service,
get_current_user,
get_memory_service,
get_message_service,
)
from ai_notes_api.db.models import User
from ai_notes_api.schemas import (
ChatMemoryResponseSchema,
ChatSessionCreateSchema,
ChatSessionListQuerySchema,
ChatSessionListResponseSchema,
Expand All @@ -27,7 +29,7 @@
MessageResponseSchema,
StatusResponseSchema,
)
from ai_notes_api.services import ChatSessionService, MessageService
from ai_notes_api.services import ChatMemoryService, ChatSessionService, MessageService

router = APIRouter(
prefix="/chat/sessions",
Expand Down Expand Up @@ -276,3 +278,43 @@ async def get_chat_session_messages(
offset=filters.offset,
total=len(messages),
)


@router.get(
"/{session_id}/memory",
summary="Get chat session memory",
description="Return memory data for a chat session.",
response_model=ChatMemoryResponseSchema,
status_code=status.HTTP_200_OK,
responses={
404: {
"model": ErrorResponseSchema,
"description": "Chat memory not found",
},
},
)
async def get_chat_session_memory(
session_id: UUID,
user: Annotated[User, Depends(get_current_user)],
service: Annotated[ChatMemoryService, Depends(get_memory_service)],
) -> ChatMemoryResponseSchema:
"""Return chat memory for a chat session.

Args:
session_id (UUID): Unique chat session identifier.
user (User): Current authenticated user.
service (ChatMemoryService): Chat memory service dependency used to
retrieve chat memory.

Returns:
ChatMemoryResponseSchema: Chat memory data.

Raises:
ChatMemoryNotFoundError: If no accessible chat memory exists for the
given chat session.
"""
logger.info("Chat session memory retrieval requested: session_id={}", session_id)

memory = await service.get_by_session_id(user.id, session_id)

return ChatMemoryResponseSchema.model_validate(memory)
48 changes: 41 additions & 7 deletions src/ai_notes_api/api/v1/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ai_notes_api.integrations import openai_client
from ai_notes_api.llm import LLMClient
from ai_notes_api.repositories import (
ChatMemoryRepository,
ChatSessionRepository,
GenerationJobRepository,
MessageRepository,
Expand All @@ -26,6 +27,7 @@
)
from ai_notes_api.services import (
AuthService,
ChatMemoryService,
ChatSessionService,
JobService,
LLMService,
Expand Down Expand Up @@ -80,7 +82,7 @@ def get_note_service(
"""
repository = NoteRepository(session)

return NoteService(repository=repository)
return NoteService(repository)


def get_auth_service(
Expand All @@ -97,7 +99,7 @@ def get_auth_service(
"""
repository = UserRepository(session)

return AuthService(repository=repository)
return AuthService(repository)


async def get_current_user(
Expand Down Expand Up @@ -133,9 +135,13 @@ def get_chat_session_service(
Returns:
ChatSessionService: Configured chat session service instance.
"""
repository = ChatSessionRepository(session)
sessions = ChatSessionRepository(session)
memories = ChatMemoryRepository(session)

return ChatSessionService(repository=repository)
return ChatSessionService(
session_repository=sessions,
memory_repository=memories,
)


def get_message_service(
Expand Down Expand Up @@ -175,9 +181,16 @@ def get_llm_service(
notes = NoteRepository(session)
messages = MessageRepository(session)
sessions = ChatSessionRepository(session)
memories = ChatMemoryRepository(session)
notes_service = NoteService(notes)
sessions_service = ChatSessionService(sessions)
messages_service = MessageService(messages, sessions)
sessions_service = ChatSessionService(
session_repository=sessions,
memory_repository=memories,
)
messages_service = MessageService(
message_repository=messages,
session_repository=sessions,
)

return LLMService(
client=client,
Expand All @@ -201,9 +214,30 @@ def get_job_service(
"""
jobs = GenerationJobRepository(session)
sessions = ChatSessionRepository(session)
sessions_service = ChatSessionService(sessions)
memories = ChatMemoryRepository(session)
sessions_service = ChatSessionService(
session_repository=sessions,
memory_repository=memories,
)

return JobService(
job_repository=jobs,
session_service=sessions_service,
)


def get_memory_service(
session: Annotated[AsyncSession, Depends(get_db)],
) -> ChatMemoryService:
"""Provide a chat memory service instance.

Args:
session (AsyncSession): Asynchronous database session provided by FastAPI
dependency injection.

Returns:
ChatMemoryService: Configured chat memory service instance.
"""
repository = ChatMemoryRepository(session)

return ChatMemoryService(repository)
2 changes: 2 additions & 0 deletions src/ai_notes_api/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

from .base import Base
from .chat_memory import ChatMemory
from .chat_session import ChatSession, ChatSessionGenerationStatus
from .datetime import SoftDeleteMixin, TimestampMixin
from .generation_job import GenerationJob, GenerationJobStatus
Expand All @@ -24,4 +25,5 @@
"GenerationJob",
"GenerationJobStatus",
"ChatSessionGenerationStatus",
"ChatMemory",
]
Loading
Loading