Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,354 changes: 1,354 additions & 0 deletions DOCUMENTATION.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion systems/docgpt/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,7 @@ pyrightconfig.json

.assets/
!.assets/.gitkeep
.localstorage
.localstorage

# Interaction logs (auto-generated by InteractionLogger)
logs/
92 changes: 64 additions & 28 deletions systems/docgpt/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
from pathlib import Path

import logging
import pypandoc
from dependency_injector.wiring import Provide, inject
from dotenv import load_dotenv
Expand All @@ -10,6 +11,7 @@
from src.app.api import create_app, run_app
from src.app.discord import BOT
from src.core import containers
from src.core.interaction_logger import init_logger
from src.domain.content import Content
from src.port.assistant import AssistantPort
from src.port.content import ContentPort
Expand All @@ -21,14 +23,31 @@
def run_terminal(
chat: AssistantPort = Provide[containers.Settings.assistant.chat],
):
from src.core.interaction_logger import get_logger

while True:
question = input("-> **Q**: ")
if question.lower() in ["q", "quit", "exit"]:
break

answer = chat.prompt(question, session_id="cli")
result = chat.prompt(question, session_id="cli")

# Log the interaction
interaction_logger = get_logger()
if interaction_logger:
try:
interaction_logger.log(
session_id="cli",
question=question,
answer=result.answer,
retrieved_context=result.retrieved_context,
source_metadata=result.source_metadata,
)
except Exception:
logger.exception("Failed to log interaction")

print(f"**-> Q: {question}\n")
print(f"**AI**: {answer}\n")
print(f"**AI**: {result.answer}\n")


@inject
Expand All @@ -52,25 +71,25 @@ def add_documents(
storage.add_documents([doc])
except Exception as e:
fails_count += 1

# Extract file information from metadata
metadata = doc.metadata if hasattr(doc, 'metadata') else {}
file_name = metadata.get('file_name', 'Unknown')
file_path = metadata.get('file_path', metadata.get('source', 'Unknown'))
project = metadata.get('project', 'Unknown')
source = metadata.get('source', 'Unknown')
metadata = doc.metadata if hasattr(doc, "metadata") else {}
file_name = metadata.get("file_name", "Unknown")
file_path = metadata.get("file_path", metadata.get("source", "Unknown"))
project = metadata.get("project", "Unknown")
source = metadata.get("source", "Unknown")

# Determine file type from file extension
file_type = 'Unknown'
if file_name and file_name != 'Unknown':
file_type = Path(file_name).suffix or 'No extension'
elif file_path and file_path != 'Unknown':
file_type = Path(file_path).suffix or 'No extension'
file_type = "Unknown"
if file_name and file_name != "Unknown":
file_type = Path(file_name).suffix or "No extension"
elif file_path and file_path != "Unknown":
file_type = Path(file_path).suffix or "No extension"

# Get exception details
exception_type = type(e).__name__
exception_message = str(e)

# Log detailed error information
logger.error(
f"Failed to ingest file - "
Expand All @@ -82,16 +101,18 @@ def add_documents(
f"Exception Type: {exception_type}, "
f"Reason: {exception_message}"
)

failed_files.append({
'file_name': file_name,
'file_type': file_type,
'file_path': file_path,
'project': project,
'source': source,
'exception_type': exception_type,
'reason': exception_message
})

failed_files.append(
{
"file_name": file_name,
"file_type": file_type,
"file_path": file_path,
"project": project,
"source": source,
"exception_type": exception_type,
"reason": exception_message,
}
)

if fails_count:
logger.warning(f"Total of {fails_count} documents failed to ingest")
Expand Down Expand Up @@ -161,10 +182,25 @@ def run_api(
application = containers.Settings()
application.config.from_yaml("config.yml", envs_required=True, required=True)
application.core.init_resources()
application.wire(modules=[__name__, "src.app.discord"])
application.wire(
modules=[
__name__,
"src.app.discord",
"src.app.api.v1.endpoints.assistant",
]
)
set_debug(True)
set_verbose(True)

# Initialise the interaction logger — logs go to INTERACTION_LOG_DIR or ./logs
log_dir = os.environ.get("INTERACTION_LOG_DIR", "logs")
interaction_logger = init_logger(output_dir=log_dir)
logger.info(
"Interaction logs: CSV=%s, JSONL=%s",
interaction_logger.csv_path,
interaction_logger.jsonl_path,
)

do_ingest, run_api_mode = _parse_args()

if do_ingest:
Expand Down
21 changes: 18 additions & 3 deletions systems/docgpt/src/adapters/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from langchain_core.vectorstores import VectorStore

from src.core.prompts import DEFAULT_PROMPT
from src.domain.assistant import Message, SessionId
from src.domain.assistant import Message, PromptResult, SessionId
from src.port.assistant import AssistantPort


Expand Down Expand Up @@ -39,7 +39,7 @@ def _get_memory(self, session_id: SessionId) -> BaseChatMemory:
def clear_history(self, session_id: SessionId) -> None:
self._get_memory(session_id).clear()

def prompt(self, message: Message, *, session_id: SessionId | None = None) -> str:
def prompt(self, message: Message, *, session_id: SessionId | None = None) -> PromptResult:
memory = self._get_memory(session_id) if session_id else None

# Build search_kwargs, only include non-None values
Expand All @@ -59,6 +59,7 @@ def prompt(self, message: Message, *, session_id: SessionId | None = None) -> st
get_chat_history=lambda v: v,
memory=memory,
verbose=True,
return_source_documents=True,
# max_tokens_limit disabled due to Gemini API compatibility issue
# max_tokens_limit=self._tokens_limit,
)
Expand All @@ -68,4 +69,18 @@ def prompt(self, message: Message, *, session_id: SessionId | None = None) -> st
qa_params["chat_history"] = ""

response = qa(qa_params)
return response["answer"]

# Extract retrieved context from source documents
source_docs = response.get("source_documents", [])
retrieved_context = "\n\n---\n\n".join(
doc.page_content for doc in source_docs
)
source_metadata = [
doc.metadata for doc in source_docs if hasattr(doc, "metadata")
]

return PromptResult(
answer=response["answer"],
retrieved_context=retrieved_context,
source_metadata=source_metadata,
)
26 changes: 24 additions & 2 deletions systems/docgpt/src/app/api/v1/endpoints/assistant.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import logging

from fastapi import APIRouter, Body, Depends, status

from src.app.api.deps import get_assistant
from src.core.interaction_logger import get_logger
from src.domain.assistant import Message, SessionId
from src.domain.responses import AssistantPromptResponse
from src.port.assistant import AssistantPort

__all__ = ("ROUTER",)

log = logging.getLogger(__name__)

ROUTER = APIRouter(prefix="/assistant", tags=['Assistant'])

@ROUTER.post("/prompt",
Expand All @@ -16,12 +22,28 @@ async def prompt(
session_id: SessionId | None = Body(None),
assistant: AssistantPort = Depends(get_assistant),
) -> AssistantPromptResponse:
answer = assistant.prompt(message, session_id=session_id)
result = assistant.prompt(message, session_id=session_id)

# Log the interaction
interaction_logger = get_logger()
if interaction_logger:
try:
interaction_logger.log(
session_id=session_id or "anonymous",
question=message,
answer=result.answer,
retrieved_context=result.retrieved_context,
source_metadata=result.source_metadata,
)
except Exception:
log.exception("Failed to log interaction")

return AssistantPromptResponse(
question=message,
session_id=session_id,
answer=answer,
answer=result.answer,
retrieved_context=result.retrieved_context,
source_count=len(result.source_metadata),
)

@ROUTER.delete("/history/{session_id}",
Expand Down
29 changes: 23 additions & 6 deletions systems/docgpt/src/app/discord.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
from langchain_text_splitters import MarkdownTextSplitter

from src.core.containers import Settings
from src.core.interaction_logger import get_logger
from src.port.assistant import AssistantPort

__all__ = ("BOT",)

log = logging.getLogger(__name__)

# Configure intents to allow fetching thread members
intents = discord.Intents.default()
intents.members = True
Expand All @@ -21,7 +24,6 @@

@BOT.event
async def on_ready():
log = logging.getLogger(__name__)
user = BOT.user
if user is None:
raise Exception("User not logged")
Expand Down Expand Up @@ -121,28 +123,43 @@ async def on_message(
user_message = await channel.fetch_message(message.id)
message_content = user_message.clean_content

response = assistant.prompt(message_content, session_id=str(channel.id))
result = assistant.prompt(message_content, session_id=str(channel.id))

# Log the interaction (question, retrieved context, answer)
interaction_logger = get_logger()
if interaction_logger:
try:
interaction_logger.log(
session_id=str(channel.id),
question=message_content,
answer=result.answer,
retrieved_context=result.retrieved_context,
source_metadata=result.source_metadata,
)
except Exception:
log.exception("Failed to log interaction")

response_chunks = MarkdownTextSplitter(
chunk_size=MAX_MESSAGE_LEN,
chunk_overlap=0,
strip_whitespace=False,
keep_separator=True,
add_start_index=True,
).split_text(response)
).split_text(result.answer)

for reply in response_chunks:
await user_message.reply(reply)

if channel.name.lower() == NEW_THREAD_NAME.lower():
title = assistant.prompt(
title_result = assistant.prompt(
f"""Create a short raw string title for this history:

- question:
{message_content}

- answer:
{response}
{result.answer}

title:"""
)
await channel.edit(name=title)
await channel.edit(name=title_result.answer)
Loading
Loading