Skip to content

Commit 190d20c

Browse files
committed
Enhance demo functionality and improve error handling
- Updated the `justfile` to include a new worker command for processing uploaded documents, improving local development setup. - Adjusted logging behavior in the API to throttle log messages based on processing counts, enhancing performance monitoring. - Modified API handlers to support Azure endpoint configuration, improving flexibility in AI integrations. - Enhanced error handling in the document processing pipeline and UI components to provide clearer feedback to users during uploads and queries. - Improved the user experience by adding contextual messages for empty responses and refining error messages in the frontend. These changes aim to streamline the development process and enhance the overall user experience in document management and AI configuration.
1 parent 3d37b4a commit 190d20c

File tree

12 files changed

+142
-30
lines changed

12 files changed

+142
-30
lines changed

demo/go/internal/api/documents.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,10 @@ func (h *Handler) listDocuments(w http.ResponseWriter, r *http.Request) {
151151
}
152152
if processingCount > 0 {
153153
now := time.Now().UnixNano()
154-
throttleNanos := int64(30 * time.Second)
154+
throttleNanos := int64(10 * time.Second)
155155
listLogMu.Lock()
156156
state, ok := listLogState[projectID]
157-
shouldLog := !ok || state.lastCount != processingCount || (now-state.lastLogUnix) > throttleNanos
157+
shouldLog := !ok || (now-state.lastLogUnix) > throttleNanos
158158
if shouldLog {
159159
listLogState[projectID] = struct{ lastLogUnix int64; lastCount int }{lastLogUnix: now, lastCount: processingCount}
160160
}

demo/go/internal/api/handler.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func NewHandler(cfg *config.Config, pool *db.Pool, store *blob.Store, pub *mq.Pu
3030
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
3131
// CORS for local/dev; tighten in production
3232
w.Header().Set("Access-Control-Allow-Origin", "*")
33-
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
33+
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
3434
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
3535
if r.Method == http.MethodOptions {
3636
w.WriteHeader(http.StatusNoContent)
@@ -147,7 +147,8 @@ func (h *Handler) putAIConfig(w http.ResponseWriter, r *http.Request) {
147147
}
148148

149149
// openaiClientFromDBConfig builds an OpenAI client from DB config (for query/stream when no key in body).
150-
func openaiClientFromDBConfig(cfg *db.AIConfigDecrypted) (*openai.Client, string) {
150+
// envAzureEndpoint is used when provider is Azure and DB endpoint is empty (e.g. key in Settings, endpoint in .env).
151+
func openaiClientFromDBConfig(cfg *db.AIConfigDecrypted, envAzureEndpoint string) (*openai.Client, string) {
151152
if cfg == nil || cfg.APIKey == "" {
152153
return nil, ""
153154
}
@@ -157,7 +158,10 @@ func openaiClientFromDBConfig(cfg *db.AIConfigDecrypted) (*openai.Client, string
157158
}
158159
switch cfg.Provider {
159160
case "azure":
160-
endpoint := cfg.Endpoint
161+
endpoint := strings.TrimSpace(cfg.Endpoint)
162+
if endpoint == "" {
163+
endpoint = strings.TrimSpace(envAzureEndpoint)
164+
}
161165
if endpoint == "" {
162166
return nil, ""
163167
}
@@ -204,7 +208,7 @@ func (h *Handler) query(w http.ResponseWriter, r *http.Request) {
204208
} else if len(h.cfg.EncryptionKey) == 32 {
205209
dbCfg, _ := h.pool.GetAIConfigDecrypted(r.Context(), h.cfg.EncryptionKey)
206210
if dbCfg != nil && dbCfg.APIKey != "" {
207-
chatClient, model = openaiClientFromDBConfig(dbCfg)
211+
chatClient, model = openaiClientFromDBConfig(dbCfg, h.cfg.AzureEndpoint)
208212
if model == "" {
209213
model = dbCfg.Model
210214
}
@@ -289,13 +293,17 @@ func (h *Handler) queryStream(w http.ResponseWriter, r *http.Request) {
289293
} else if len(h.cfg.EncryptionKey) == 32 {
290294
dbCfg, _ := h.pool.GetAIConfigDecrypted(ctx, h.cfg.EncryptionKey)
291295
if dbCfg != nil && dbCfg.APIKey != "" {
292-
streamClient, model = openaiClientFromDBConfig(dbCfg)
296+
streamClient, model = openaiClientFromDBConfig(dbCfg, h.cfg.AzureEndpoint)
293297
if model == "" {
294298
model = dbCfg.Model
295299
}
296300
}
297301
}
298302
if err := h.rag.StreamCompletionWithClient(ctx, prompt, w, streamClient, model); err != nil {
303+
_ = enc.Encode(map[string]any{"error": err.Error()})
304+
if f, ok := w.(http.Flusher); ok {
305+
f.Flush()
306+
}
299307
return
300308
}
301309
}

demo/go/internal/config/config.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package config
22

33
import (
4+
"log"
45
"os"
6+
"strings"
57

68
"github.com/rithulkamesh/docproc/demo/internal/secure"
79
"github.com/sashabaranov/go-openai"
810
)
911

12+
// DefaultEncryptionPassphrase is used when DOCPROC_ENCRYPTION_KEY is not set (dev only; set your own in production).
13+
const DefaultEncryptionPassphrase = "docproc-dev-default-change-in-production"
14+
1015
// Config holds demo app configuration (env or file).
1116
type Config struct {
1217
DatabaseURL string // PostgreSQL (documents, projects, pgvector)
@@ -36,7 +41,7 @@ func Load() (*Config, error) {
3641
S3Region: getEnv("AWS_REGION", "us-east-1"),
3742
MQURL: getEnv("MQ_URL", "amqp://docproc:docproc@localhost:5672/"),
3843
DocprocCLI: getEnv("DOCPROC_CLI", "docproc"),
39-
EncryptionKey: keyFromEnvOrNil(os.Getenv("DOCPROC_ENCRYPTION_KEY")),
44+
EncryptionKey: keyFromEnvOrDefault(os.Getenv("DOCPROC_ENCRYPTION_KEY")),
4045
OpenAIKey: os.Getenv("OPENAI_API_KEY"),
4146
OpenAIModel: getEnv("OPENAI_MODEL", "gpt-4o-mini"),
4247
AzureAPIKey: os.Getenv("AZURE_OPENAI_API_KEY"),
@@ -103,10 +108,11 @@ func getEnv(key, defaultVal string) string {
103108
return defaultVal
104109
}
105110

106-
// keyFromEnvOrNil returns a 32-byte key derived from passphrase, or nil if passphrase is empty.
107-
func keyFromEnvOrNil(passphrase string) []byte {
108-
if passphrase == "" {
109-
return nil
111+
// keyFromEnvOrDefault returns a 32-byte key derived from passphrase. If passphrase is empty, uses a default (dev only; log warning).
112+
func keyFromEnvOrDefault(passphrase string) []byte {
113+
if strings.TrimSpace(passphrase) == "" {
114+
log.Print("[config] DOCPROC_ENCRYPTION_KEY not set; using default (fine for dev; set your own in production)")
115+
return secure.KeyFromEnv(DefaultEncryptionPassphrase)
110116
}
111117
return secure.KeyFromEnv(passphrase)
112118
}

demo/go/internal/db/db.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@ package db
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

78
"github.com/jackc/pgx/v5"
9+
"github.com/jackc/pgx/v5/pgconn"
810
"github.com/jackc/pgx/v5/pgxpool"
911
pgxvec "github.com/pgvector/pgvector-go/pgx"
1012
)
1113

14+
// isDuplicateExtensionError returns true if err is a unique violation on pg_extension (extension already exists).
15+
func isDuplicateExtensionError(err error) bool {
16+
var pgErr *pgconn.PgError
17+
if !errors.As(err, &pgErr) {
18+
return false
19+
}
20+
return pgErr.Code == "23505" && pgErr.ConstraintName == "pg_extension_name_index"
21+
}
22+
1223
// Pool is the PostgreSQL connection pool.
1324
type Pool struct {
1425
*pgxpool.Pool
@@ -28,7 +39,10 @@ func NewPool(ctx context.Context, connString string) (*Pool, error) {
2839
_, err = conn.Exec(ctx, `CREATE EXTENSION IF NOT EXISTS vector`)
2940
conn.Close(ctx)
3041
if err != nil {
31-
return nil, fmt.Errorf("vector extension: %w", err)
42+
// Another process (e.g. API and worker both starting) may have created it; duplicate key is OK.
43+
if !isDuplicateExtensionError(err) {
44+
return nil, fmt.Errorf("vector extension: %w", err)
45+
}
3246
}
3347
config.AfterConnect = func(ctx context.Context, c *pgx.Conn) error {
3448
return pgxvec.RegisterTypes(ctx, c)

demo/go/internal/rag/rag.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func (r *RAG) QueryWithClient(ctx context.Context, question string, client *open
9999
return "", sources, fmt.Errorf("chat: %w", err)
100100
}
101101
if len(chatResp.Choices) == 0 {
102-
return "", sources, nil
102+
return "", sources, fmt.Errorf("model returned no choices")
103103
}
104104
answer = strings.TrimSpace(chatResp.Choices[0].Message.Content)
105105
return answer, sources, nil

demo/justfile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ deps:
1010
docker compose up -d
1111
@echo "Waiting for postgres..."
1212
sleep 3
13-
@echo "Deps up. Run: just dev (API + web in one terminal, Ctrl+C kills both)"
13+
@echo "Deps up. Run: just dev (API + worker + web in one terminal, Ctrl+C kills all)"
1414

15-
# Run API + web locally; logs from both in one terminal. Ctrl+C kills both.
15+
# Run API + web + worker locally; logs from all in one terminal. Ctrl+C kills all.
16+
# Worker processes uploaded documents (needs docproc CLI on PATH: pip install docproc, or python -m docproc.bin.cli).
1617
dev: deps
1718
#!/usr/bin/env bash
1819
set -e
@@ -21,6 +22,7 @@ dev: deps
2122
export S3_ENDPOINT="http://localhost:4566"
2223
trap 'kill $(jobs -p) 2>/dev/null; exit 130' INT TERM
2324
(cd go && go run .) &
25+
(cd go && go run . -worker) &
2426
(cd web && VITE_DOCPROC_API_URL=http://localhost:8080 npm run dev) &
2527
wait
2628

@@ -35,6 +37,14 @@ api:
3537
export S3_ENDPOINT="http://localhost:4566"
3638
cd go && go run .
3739

40+
# Run document processing worker only. Consumes jobs from RabbitMQ, runs docproc CLI.
41+
# Requires docproc on PATH (e.g. pip install -e . from repo root, or set DOCPROC_CLI).
42+
worker:
43+
export DATABASE_URL="postgresql://docproc:docproc@localhost:5432/docproc?sslmode=disable"
44+
export MQ_URL="amqp://docproc:docproc@localhost:5672/"
45+
export S3_ENDPOINT="http://localhost:4566"
46+
cd go && go run . -worker
47+
3848
# Run Go API with live reload (install: go install github.com/air-verse/air@latest)
3949
api-watch:
4050
export DATABASE_URL="postgresql://docproc:docproc@localhost:5432/docproc?sslmode=disable"

demo/web/src/api/query.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export async function runQueryStream(
7878
if (!trimmed) continue
7979
try {
8080
const data = JSON.parse(trimmed) as Record<string, unknown>
81+
if (typeof data.error === 'string') {
82+
callbacks.onError(data.error)
83+
return false
84+
}
8185
if (Array.isArray(data.sources)) {
8286
callbacks.onSources(data.sources as RagSource[])
8387
} else if (typeof data.delta === 'string') {

demo/web/src/components/canvas/ConverseCanvas.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function normalizeQueryError(msg: string): string {
9393
return "AI provider isn't set up correctly. Check your config."
9494
}
9595
if (msg.includes('api_key') || msg.includes('401') || msg.includes('403')) {
96-
return 'API key missing or invalid.'
96+
return 'API key missing or invalid. Set OPENAI_API_KEY or AZURE_OPENAI_API_KEY in .env, or add your key in Settings → AI provider.'
9797
}
9898
return msg
9999
}
@@ -211,6 +211,13 @@ export function ConverseCanvas() {
211211
},
212212
onDone: () => {
213213
stopStreamFlush()
214+
setMessages((prev) =>
215+
prev.map((m) =>
216+
m.id === assistantId && !m.content.trim()
217+
? { ...m, content: 'No response from the model. Check your AI config or try again.' }
218+
: m
219+
)
220+
)
214221
setSending(false)
215222
},
216223
onError: applyError,
@@ -221,6 +228,15 @@ export function ConverseCanvas() {
221228
if (res.answer.startsWith('Query failed:')) {
222229
const raw = res.answer.replace(/^Query failed:\s*/, '').trim()
223230
applyError(raw)
231+
} else if (!res.answer.trim()) {
232+
setMessages((prev) =>
233+
prev.map((m) =>
234+
m.id === assistantId
235+
? { ...m, content: 'No response from the model. Check your AI config or try again.' }
236+
: m
237+
)
238+
)
239+
setSending(false)
224240
} else {
225241
// Simulate streaming for non-streaming API so UX is consistent
226242
const full = res.answer

demo/web/src/components/canvas/SourcesCanvas.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export function SourcesCanvas({ welcomeProjectName = null }: SourcesCanvasProps)
2121
setSelectedDocumentId,
2222
setCanvasMode,
2323
handleUploadFile,
24+
loadDocuments,
2425
handleDeleteDocument,
2526
handleReindexDocument,
2627
apiStatusLabel,
@@ -39,18 +40,24 @@ export function SourcesCanvas({ welcomeProjectName = null }: SourcesCanvasProps)
3940
const list = Array.from(files)
4041
setUploadError(null)
4142
setUploadingCount(list.length)
43+
const isBatch = list.length > 1
4244
let lastError: string | null = null
4345
for (const file of list) {
4446
try {
45-
await handleUploadFile(file)
47+
await handleUploadFile(file, isBatch ? { skipListRefresh: true } : undefined)
4648
} catch (e) {
4749
lastError = e instanceof Error ? e.message : 'Upload failed'
4850
}
4951
}
52+
if (isBatch) {
53+
try {
54+
await loadDocuments()
55+
} catch {}
56+
}
5057
setUploadingCount(0)
5158
if (lastError) setUploadError(lastError)
5259
},
53-
[handleUploadFile]
60+
[handleUploadFile, loadDocuments]
5461
)
5562

5663
const handleUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {

demo/web/src/context/WorkspaceContext.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ interface WorkspaceContextValue {
2222
setSelectedDocumentId: (id: string | null) => void
2323
loadProjects: () => Promise<void>
2424
loadDocuments: () => Promise<void>
25-
handleUploadFile: (file: File) => Promise<void>
25+
handleUploadFile: (file: File, options?: { skipListRefresh?: boolean }) => Promise<void>
2626
handleDeleteDocument: (documentId: string) => Promise<void>
2727
handleReindexDocument: (documentId: string) => Promise<void>
2828
status: ApiStatus | null
@@ -240,9 +240,10 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
240240
)
241241

242242
const handleUploadFile = useCallback(
243-
async (file: File) => {
243+
async (file: File, options?: { skipListRefresh?: boolean }) => {
244244
try {
245245
await uploadDocument(file, currentProjectId)
246+
if (options?.skipListRefresh) return
246247
const docs = await listDocuments(currentProjectId)
247248
setDocuments(docs)
248249
if (!selectedDocumentId && docs.length > 0) {

0 commit comments

Comments
 (0)