Skip to content

Implement Chat#1

Open
miqdadyyy wants to merge 2 commits into
mainfrom
feat/realtime-chat
Open

Implement Chat#1
miqdadyyy wants to merge 2 commits into
mainfrom
feat/realtime-chat

Conversation

@miqdadyyy

Copy link
Copy Markdown
Owner

No description provided.

Comment thread .gitignore
.idea/
out/
tmp/
storage/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider keeping the directory structure with a .gitkeep

Ignoring storage/ means fresh clones won't have the directory. If the app expects it at runtime, add storage/.gitkeep (and !storage/.gitkeep in .gitignore) or create it programmatically.

Suggested change
storage/
storage/*
!storage/.gitkeep

Comment thread cmd/http/routes/space.go
router.Post("/:id/documents", h.UploadSpaceDocument)
router.Get("/:id/documents/status", h.GetDocumentStatusStream)
router.Delete("/:id", h.DeleteSpace)
router.Delete("/:id/documents/:documentId", h.DeleteSpaceDocument)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential route conflict: wildcard /:id vs /status segment

Fiber matches routes top-down. /:id/documents/status is registered after /:id/documents (POST). If a GET /:id/documents/:documentId were ever added above it, status could be captured as a documentId. Current ordering is fine, but consider registering more-specific paths before less-specific ones or adding a constraint.

Comment thread cmd/http/routes/space.go

router.Get("/:id/conversation", h.GetConversationList)
router.Post("/:id/conversation", h.CreateNewConversation)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REST convention: plural 'conversations' preferred

Other sub-resources use plural (documents). For consistency, consider /:id/conversations instead of /:id/conversation.

Suggested change
}
router.Get("/:id/conversations", h.GetConversationList)
router.Post("/:id/conversations", h.CreateNewConversation)

Comment thread cmd/http/main.go
AppName: "Default Example Go Project App",
BodyLimit: 10 * 1024 * 1024,
AppName: cfg.App.Name,
BodyLimit: 1024 * 1024 * 1024,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BodyLimit 1GB is excessive – DoS risk

1GB body limit exposes server to memory exhaustion / slowloris-style attacks. Use a sensible default (e.g. 10–50 MB) or make it configurable via cfg with an upper-bound guard.

Suggested change
BodyLimit: 1024 * 1024 * 1024,
BodyLimit: cfg.App.BodyLimitMB * 1024 * 1024,

Comment thread cmd/http/main.go
app := fiber.New(fiber.Config{
AppName: "Default Example Go Project App",
BodyLimit: 10 * 1024 * 1024,
AppName: cfg.App.Name,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure cfg.App.Name has a fallback

If cfg.App.Name is empty the Fiber app name will be blank. Consider a default: cmp.Or(cfg.App.Name, "my-service") or validate at config load time.

type GenerateStreamChunk struct {
Content string
Done bool
InputTokens int

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err field in data struct

Embedding Err in the chunk struct couples error signaling with data. If this is sent over a channel, consumers must check Err on every receive. Consider using a separate error channel or a wrapper type to make the contract explicit. Not a bug, but worth documenting the expected consumer pattern.

)

type GenerateStreamChunk struct {
Content string

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token counts only meaningful when Done==true

InputTokens/OutputTokens likely only populated on the final chunk (Done==true). Add a doc comment clarifying when these fields are valid to prevent misuse.

Suggested change
Content string
// GenerateStreamChunk represents a single chunk in a streaming response.
// InputTokens, OutputTokens, and Duration are only populated when Done is true.
type GenerateStreamChunk struct {

Comment thread cmd/http/wire.go
import (
"github.com/google/wire"
"github.com/miqdadyyy/gorago/internal/pkg/repository"
conversationrpi "github.com/miqdadyyy/gorago/internal/repository/conversation/implementation"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent alias naming convention

conversationrpi uses rpi suffix while spacerpi also uses rpi, but usecase aliases use uci. The repo alias pattern rpi doesn't match a clear abbreviation (repository implementation → rpi vs ri). Consider aligning all aliases to a single convention, e.g., conversationri/spaceri or conversationrpi/spacerpi — currently consistent for repos but differs from the old spaceuc (now spaceuci). Minor readability concern only.

Comment thread docs/http/swagger.json
@@ -259,6 +710,24 @@
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid schema: metadata field has no type

"metadata": {} is not a valid Swagger 2.0 schema object. It must have at least a type or a $ref. This will cause spec validation failures and code-gen issues.

Suggested change
}
"metadata": {
"type": "object"
},

Comment thread docs/http/swagger.json
@@ -241,6 +673,25 @@
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant conversation_id in SendMessageRequest body

The endpoint path is /api/conversation/{id}/message which already carries the conversation ID. Having conversation_id in the request body is redundant and could cause confusion if they differ. Consider removing it from the body schema or documenting precedence.

Comment thread docs/http/swagger.json
],
"responses": {
"200": {
"description": "OK",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POST create conversation should return 201

Creating a new conversation is a resource-creation operation. HTTP semantics recommend 201 Created rather than 200 OK.

Suggested change
"description": "OK",
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/github_com_miqdadyyy_gorago_internal_entity.CreateNewConversationResponse"
}
},

Comment thread docs/http/swagger.json
@@ -17,7 +17,157 @@
},
"basePath": "/",
"paths": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No security/auth definitions

None of the new endpoints declare any security scheme. If these endpoints require authentication, add a securityDefinitions section and reference it in each operation.

Comment thread docs/http/swagger.json
@@ -184,9 +462,109 @@
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSE endpoint missing consumes field

The /api/space/{id}/documents/status GET endpoint does not declare a consumes field, which is fine for GET, but the sibling SSE endpoint /api/conversation/{id}/message (POST) declares consumes: application/json while producing text/event-stream. Ensure consistency—POST SSE endpoint should still validate JSON input but document that the response is not JSON.

package entity

import (
conversationucm "github.com/miqdadyyy/gorago/internal/usecase/conversation/models"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entity layer depends on usecase layer

entity pkg imports from usecase (conversationucm). This inverts the typical dependency direction (usecase → entity). If usecase changes, entity must change. Move ConversationMessageItem to entity or a shared models pkg to keep entity at the bottom of the dependency graph.

package entity

import (
conversationucm "github.com/miqdadyyy/gorago/internal/usecase/conversation/models"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import grouping

Standard library imports should be in a separate group from third-party imports per Go convention.

Suggested change
conversationucm "github.com/miqdadyyy/gorago/internal/usecase/conversation/models"
import (
"time"
conversationucm "github.com/miqdadyyy/gorago/internal/usecase/conversation/models"
)


ProcessDocumentPageIngestionRequest struct {
SpaceDocumentID int64 `json:"space_document_id"`
PageNumber int `json:"page_number"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding validation constraint for PageNumber

PageNumber has zero-value 0 which may be valid or invalid depending on context. If pages are 1-indexed, consider adding a validate:"min=1" tag or documenting expected range.

Comment thread config/local.yaml
@@ -51,11 +40,11 @@ log:
rotation: false

storage:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded credentials committed

Real access_key and secret_access_key values ('miqdad', 'monalisa123') are committed to source control. This is a security risk → rotate these credentials immediately and use environment variable references or placeholder values.

Suggested change
storage:
access_key: "${STORAGE_ACCESS_KEY}"
secret_access_key: "${STORAGE_SECRET_ACCESS_KEY}"

Comment thread config/local.yaml
provider: "ollama"
ollama:
host: "http://localhost:11434"
base_url: "http://192.168.1.23:11434"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal/private IP address hardcoded

Private IP 192.168.1.23 is environment-specific and will break for other developers. Use localhost or an env var.

Suggested change
base_url: "http://192.168.1.23:11434"
base_url: "http://localhost:11434"

Comment thread config/local.yaml
ollama:
host: "http://localhost:11434"
model: "gemma4:e2b"
base_url: "http://192.168.1.23:11434"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal/private IP address hardcoded (embedding)

Same private IP repeated for embedding provider. Use localhost or env var.

Suggested change
base_url: "http://192.168.1.23:11434"
base_url: "http://localhost:11434"

Comment thread config/local.yaml
max_tokens: 500
overlap: 50 No newline at end of file
max_tokens: 800
overlap: 150 No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No newline at end of file

File should end with a newline to comply with POSIX and avoid noisy diffs.

Suggested change
overlap: 150
overlap: 150


// Create independent context — Fiber's request context may be cancelled
// when the handler returns, but SendStreamWriter runs asynchronously.
streamCtx, cancel := context.WithCancel(context.Background())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detached context won't propagate client cancellation to usecase

Using context.Background() means if the client disconnects before SendStreamWriter starts (or during channel setup), the usecase has no signal to stop. Consider deriving from a request-scoped context or wiring Fiber's close-notify into streamCtx.

Suggested change
streamCtx, cancel := context.WithCancel(context.Background())
streamCtx, cancel := context.WithCancel(ctx.Context())

_ = r
}
}()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.Marshal error silently ignored → may send null to client

If json.Marshal(event.Data) fails, data will be nil and the SSE frame will contain literal null. Handle the error or at minimum skip the event.

Suggested change
data, merr := json.Marshal(event.Data)
if merr != nil {
continue
}


return ctx.SendStreamWriter(func(w *bufio.Writer) {
defer cancel()
defer func() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recover block is a no-op

Recovering the panic and assigning to _ hides bugs silently. At minimum log the recovered value so issues are observable in production.

Suggested change
defer func() {
defer func() {
if r := recover(); r != nil {
// TODO: replace with structured logger
fmt.Printf("SendMessage stream panic: %v\n", r)
}
}()


res, errs := h.conversationUsecase.CreateNewConversation(ctx.Context(), req)
if errs != nil {
return response.New(errs.GetHttpCode(), errs.GetMessage(), nil).Render(ctx)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP 201 more appropriate for resource creation

CreateNewConversation returns 200; 201 Created is the conventional status for successful resource creation.

Suggested change
return response.New(errs.GetHttpCode(), errs.GetMessage(), nil).Render(ctx)
return response.New(http.StatusCreated, "Successfully created new conversation", res).Render(ctx)

Comment thread cmd/http/wire_gen.go
implementation4 "github.com/miqdadyyy/gorago/internal/usecase/space/implementation"
implementation6 "github.com/miqdadyyy/gorago/internal/usecase/space/implementation"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate import block

Two separate import blocks; idiomatic Go and goimports merge them into one. This may indicate manual editing of a generated file or a stale codegen template.

Comment thread cmd/http/wire_gen.go
iPubSub := provider.ProvidePubSub(config)
iExampleUsecase := implementation2.NewExampleUsecase(exampleRepositoryInterface, iPubSub)
db := provider.ProvideDatabase(config)
iEmbedder := provider.ProvideEmbedder(config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: illm variable name

illm looks like a typo (lowercase L + llm). Consider renaming to iLLM for consistency with other iXxx variables. Since this is generated code, the fix belongs in the provider return-type or wire configuration.

Comment thread cmd/http/wire_gen.go
iExampleUsecase := implementation2.NewExampleUsecase(exampleRepositoryInterface, iPubSub)
db := provider.ProvideDatabase(config)
iEmbedder := provider.ProvideEmbedder(config)
illm := provider.ProvideLLM(config)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iStorage declared before db but depends on config only – ordering fine, but readability concern

Provider ordering changed: iStorage now appears before db. Functionally fine (both depend only on config), but the previous grouping (DB → cache → encryption → base repo) was clearer. Ensure the wire injector graph is intentional.

Comment thread cmd/http/wire_gen.go
iConversationMessageRepository := implementation3.NewConversationMessageRepository(baseRepository)
iSpaceDocumentRepository := implementation4.NewSpaceDocumentRepository(baseRepository)
iSpaceDocumentChunkRepository := implementation4.NewSpaceDocumentChunk(baseRepository)
iConversationUsecase := implementation5.NewConversationUsecase(iEmbedder, illm, iStorage, iConversationRepository, iConversationMessageRepository, iSpaceDocumentRepository, iSpaceDocumentChunkRepository)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iSpaceDocumentChunkRepository not used in iSpaceUsecase

iSpaceDocumentChunkRepository is created and passed only to iConversationUsecase. If SpaceUsecase also needs chunk access in the future, this wiring may need updating. Verify the conversation usecase truly owns chunk queries and not the space usecase.

Comment thread internal/entity/space.go
SpaceID int64 `json:"space_id"`
}

DocumentStatusEvent struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IngestionStatus uses raw int

Consider a typed constant (enum) for IngestionStatus to improve readability and prevent invalid values.

Comment thread go.sum
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate pgx/v5 entries – run go mod tidy

Both v5.6.0 and v5.7.2 of jackc/pgx/v5 present in go.sum. Only one version should remain after go mod tidy. Keeping both suggests the go.mod upgrade wasn't finalized properly.

Comment thread go.sum
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate rogpeppe/go-internal entries

v1.6.1 h1 line already exists (line 360), and v1.9.0 h1 is added without removing the old one. This is fine for go.sum technically (indirect deps can have multiple), but if v1.9.0 is the required version, the old h1 line is dead weight. Run go mod tidy.

Comment thread web/src/lib/utils/sse.ts
for (const line of part.split('\n')) {
if (line.startsWith('event: ')) {
eventType = line.slice(7).trim();
} else if (line.startsWith('data: ')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-line data: fields overwritten instead of concatenated

Per SSE spec, an event can have multiple data: lines that should be joined with \n. Current code overwrites eventData on each data: line → only last line kept.

Suggested change
} else if (line.startsWith('data: ')) {
} else if (line.startsWith('data: ')) {
eventData += (eventData ? '\n' : '') + line.slice(6);
}

Comment thread web/src/lib/utils/sse.ts
// Split on double newline to get complete events
const parts = buffer.split('\n\n');
// Last part may be incomplete — keep it in the buffer
buffer = parts.pop() ?? '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remaining buffer after stream ends is discarded

If the stream closes without a trailing \n\n, the last event in buffer is never processed. Consider processing buffer after the loop exits if it contains data.

Comment thread web/src/lib/utils/sse.ts
eventData = line.slice(6);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Events with empty data field are silently skipped

if (!eventType || !eventData) continue; means a valid event with an empty JSON body (e.g., data: "") is ignored. Likely fine for this protocol but worth documenting.


Always be concise.`

PromptBuilderMaxContextCharacters = 12000

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document magic number rationale

PromptBuilderMaxContextCharacters = 12000 lacks explanation. Consider adding a comment explaining why 12000 was chosen (e.g., token budget estimation, model context window constraints).

Suggested change
PromptBuilderMaxContextCharacters = 12000
// PromptBuilderMaxContextCharacters is the maximum number of characters allowed in the context.
// Approximates ~3000 tokens to leave room for system prompt, history, and response within model limits.
PromptBuilderMaxContextCharacters = 12000

Message conversationucm.ConversationMessageItem `json:"message"`
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panic recovery sends to closed channel

In the goroutine, defer close(ch) is declared before defer func() { recover... ch <- ... }(). Defers execute LIFO, so recovery runs first (good), but if the panic happens after the channel is already being read and the consumer has exited, sending to ch after close will panic again. More critically, if the panic occurs during the final ch <- send itself, the recovery will try to send on a potentially full/closed channel. Reorder defers or use a select with default when sending in recovery.

func (u *ConversationUsecase) SendMessage(
ctx context.Context,
request entity.SendMessageRequest,
) (<-chan entity.SSEEvent, errors.IError) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential nil pointer dereference in spaceDocumentMap lookup

If spaceDocumentChunk.SpaceDocumentID is not found in spaceDocumentMap (e.g., document was deleted between queries), spaceDocumentMap[spaceDocumentChunk.SpaceDocumentID] returns nil, and accessing .FilePath will panic. Add a nil check.

req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
o.cfg.BaseURL+"/api/generate",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

streamHTTPClient has no timeout — potential goroutine/connection leak

If the context is never cancelled (e.g., caller forgets), the HTTP connection and goroutine will hang indefinitely. Consider adding a generous but finite timeout (e.g., 10 minutes) as a safety net, or document that callers MUST cancel the context.

spaceDocumentChunks, err := u.spaceDocumentChunkRepository.GetSpaceDocumentChunkByVector(
ctx,
spacerpm.GetSpaceDocumentByVectorPayload{
Vector: vectorQuery,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handler creates context.Background() — no request-scoped deadline

Using context.Background() means there's no upper bound on how long the streaming goroutine can run. If the client disconnects and Fiber doesn't signal the writer, the usecase goroutine runs forever. Consider deriving from a timeout context or detecting writer errors to call cancel().

Type: entity.SSEEventTypeStatus,
Data: map[string]string{"phase": "generating"},
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writer error detection doesn't cancel upstream context

When w.Flush() returns an error (client disconnected), the handler returns but cancel() is deferred so it will fire. This is correct. However, the usecase goroutine may still be blocked sending to eventCh if the buffer is full. Since the handler stops reading, the channel send in the usecase will block until GC. Consider draining the channel or using a select-based send in the usecase.

}

ch := make(chan llm.GenerateStreamChunk, 32)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scanner default buffer may be too small for large JSON chunks

bufio.Scanner has a default max token size of 64KB. If Ollama ever returns a line larger than that (unlikely but possible with large prompt_eval metadata), the scanner will fail silently. Consider scanner.Buffer(make([]byte, 0, 256*1024), 256*1024) for safety.

}
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSE parser doesn't handle multi-line data fields

The SSE spec allows multiple data: lines concatenated with newlines. This parser only captures the last data: line. If the server ever emits multi-line data, parsing will break. For this use case (JSON on single line) it's fine, but worth a comment.

}
}()

return ch, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing SendMessageResponse may break other consumers

Step 2 of Task 4 removes SendMessageResponse struct. If any other handler, test, or mock references this type, compilation will fail. The plan acknowledges checking imports but doesn't verify all usages across the codebase (tests, mocks, etc.).

ConversationMessageItem struct {
Role string `json:"role"`
Content string `json:"content"`
Metadata any `json:"metadata"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untyped Metadata field

any loses type safety and complicates serialization/deserialization. Consider a concrete type, map[string]any, or json.RawMessage to make intent explicit.

Suggested change
Metadata any `json:"metadata"`
Metadata map[string]any `json:"metadata"`

Role string `json:"role"`
Content string `json:"content"`
Metadata any `json:"metadata"`
CreatedAt *time.Time `json:"created_at"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointer time fields – consider omitempty

Pointer *time.Time will serialize as null when nil. Add omitempty if null timestamps should be omitted from JSON output.

Suggested change
CreatedAt *time.Time `json:"created_at"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`

**Update `SendMessage` handler** (`internal/handler/http/conversation.go`):

- Parse and validate request (conversation ID, body)
- Call `conversationUsecase.SendMessage()` — if error returned, respond with normal JSON error (400/404)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.Marshal error silently discarded

The handler code sample discards the error from json.Marshal(event.Data). If marshaling fails (e.g., a channel or func value in Data), the stream will emit malformed/empty JSON. Handle the error or at minimum log it and send an error event.

Suggested change
- Call `conversationUsecase.SendMessage()` — if error returned, respond with normal JSON error (400/404)
data, err := json.Marshal(event.Data)
if err != nil {
fmt.Fprintf(w, "event: error\ndata: {\"message\":\"internal marshal error\"}\n\n")
w.Flush()
cancel()
return
}
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, data)

12. Save assistant message to DB (with accumulated content + metadata)
13. **Push `done` with the full saved message**
14. Close channel (via defer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context.Background() loses request-scoped values

Using context.WithCancel(context.Background()) discards tracing spans, auth info, and any request-scoped values. Consider deriving from a detached copy of the request context that preserves values but not the cancellation, or explicitly propagate needed values.

5. Embed query via embedder
6. Vector search for relevant chunks
7. Build RAG prompt
8. **Push `status: generating`**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No SSE keepalive/heartbeat mechanism

Reverse proxies (nginx, ALB) and browsers may close idle connections after 30-60s. The spec should define a periodic heartbeat comment line (: keepalive\n\n) during long-running phases like embedding or vector search to prevent premature connection drops.

SSEEventTypeError SSEEventType = "error"
)

type SSEEvent struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No backpressure strategy for slow clients

If the client reads slowly, the buffered channel (size 32) will fill and the usecase goroutine will block, holding LLM connection resources. Consider documenting a timeout on channel sends or a select with context cancellation to avoid goroutine leaks.

}
```

Returns a **channel and an error**. The error return handles pre-stream validation failures (conversation not found, invalid request) so the handler can return proper HTTP 400/404 status codes. If `err` is nil, the channel is valid and streaming begins. The channel uses a **buffer size of 32**.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial content not saved but user message is — inconsistent state

Step 2 saves the user message before streaming begins. If the LLM fails mid-stream, the user message persists but no assistant message is saved (per error handling section). This leaves the conversation in an inconsistent state with a dangling user message and no response. Document the expected recovery path (retry? delete user msg?).

### 4. HTTP Handler

**Update `SendMessage` handler** (`internal/handler/http/conversation.go`):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fmt.Fprintf return error unchecked

fmt.Fprintf(w, ...) can also return an error (e.g., broken pipe). Only w.Flush() is checked. If Fprintf fails, the subsequent Flush may succeed on buffered data, delaying disconnect detection. Consider checking Fprintf's error as well.

Suggested change
if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, data); err != nil {
cancel()
return
}
if err := w.Flush(); err != nil {
cancel()
return
}


type ILLM interface {
Generate(ctx context.Context, payload GenerateRequest) (response GenerateResponse, err error)
GenerateStream(ctx context.Context, payload GenerateRequest) (<-chan GenerateStreamChunk, error)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No mid-stream error propagation mechanism

Returning only <-chan GenerateStreamChunk means errors occurring during streaming (after the channel is created) can only be communicated via a field inside GenerateStreamChunk. Ensure GenerateStreamChunk contains an Err error field (or similar) so consumers can detect partial failures without a separate side-channel.

logger := log.WithContext(ctx)
outputPath = fmt.Sprintf("%s/", getDocumentDirectory(path))
if err = api.ExtractPagesFile(path, outputPath, nil, nil); err != nil {
if err = os.MkdirAll(outputPath, os.ModePerm); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overly permissive directory permissions

os.ModePerm (0777) grants world-read/write/execute. Use 0750 or 0700 unless broader access is explicitly needed.

Suggested change
if err = os.MkdirAll(outputPath, os.ModePerm); err != nil {
if err = os.MkdirAll(outputPath, 0750); err != nil {

return "", err
}

if err = api.ExtractContentFile(path, outputPath, nil, nil); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API changed from ExtractPagesFile to ExtractContentFile

ExtractContentFile extracts raw content streams (not rendered page PDFs). If the intent is to extract individual page PDF files, this should remain ExtractPagesFile. If the change is intentional, the error message and function name are misleading.

Suggested change
if err = api.ExtractContentFile(path, outputPath, nil, nil); err != nil {
if err = api.ExtractPagesFile(path, outputPath, nil, nil); err != nil {


func ProvidePDF() pdf.IPDF {
return pdf.NewPDFCpu()
return pdf.NewPDFPoppler()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

External binary dependency not validated

PDFPoppler typically shells out to poppler-utils (pdftotext, pdftoppm). If these aren't present in the deployment environment the call will fail at runtime. Consider adding a startup check (e.g., exec.LookPath) or documenting the new dependency in Dockerfile/CI.

if err != nil {
panic("failed to connect to database")
}
//_ = db.Migrator().DropTable(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented-out DropTable code

Dead commented-out code adds noise. Remove or gate behind a flag/env var if needed for dev.

// &spacerpm.SpaceDocumentChunk{},
//)

_ = db.AutoMigrate(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoMigrate error ignored

Ignoring AutoMigrate error (_ =) can hide schema issues silently → hard-to-debug runtime failures. Handle or at least log.

Suggested change
_ = db.AutoMigrate(
if err := db.AutoMigrate(
&conversationrpm.Conversation{},
&conversationrpm.ConversationMessage{},
&conversationrpm.ConversationMessageContext{},
&spacerpm.Space{},
&spacerpm.SpaceDocument{},
&spacerpm.SpaceDocumentIngestion{},
&spacerpm.SpaceDocumentChunk{},
); err != nil {
panic("failed to auto-migrate: " + err.Error())
}


func ProvideLLM(cfg config.Config) llm.ILLM {
return ollama.NewOllamaLLM(cfg.LLM.Ollama.BaseURL)
return ollama.NewOllamaLLM(cfg.LLM.Ollama)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure NewOllamaLLM signature matches new arg

Signature changed from BaseURL string to full Ollama config struct. Verify NewOllamaLLM accepts cfg.LLM.Ollama type and all callers/tests updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant