Files
descrybe/apps/api/cmd/_final_verify_quick_tmp/main.go
T
2026-08-17 09:33:07 +02:00

410 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Temporary slim LIVE verify — 2 products, short timeouts. Do not commit.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprovider"
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
"github.com/descrybe/descrybe-v2/apps/api/internal/db"
"github.com/descrybe/descrybe-v2/apps/api/internal/eprel"
"github.com/descrybe/descrybe-v2/apps/api/internal/logredact"
"github.com/descrybe/descrybe-v2/apps/api/internal/platformsettings"
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
"github.com/google/uuid"
)
func main() {
out := `F:\laragon\www\_MY\descrybe-v2\.codehelper\_final_verify_20260817`
_ = os.MkdirAll(out, 0o755)
logPath := filepath.Join(out, "inproc_run_quick.log")
logFile, err := os.Create(logPath)
if err != nil {
log.Fatalf("log: %v", err)
}
defer logFile.Close()
log.SetOutput(logredact.Writer(io.MultiWriter(os.Stderr, logFile)))
repoRoot := `F:\laragon\www\_MY\descrybe-v2`
wpPath := filepath.Join(repoRoot, "scripts", "seed", "wp_product_categories.sql")
wpBytes, err := os.ReadFile(wpPath)
if err != nil {
failReport(out, "FAIL", "repo seed missing: "+err.Error(), nil)
}
_ = os.Setenv("SEED_A1_WP_CATEGORIES", wpPath)
eans := []string{
"4548736132597", // Slušalke
"195348253666", // Gaming monitorji
}
companyID := uuid.MustParse("604f23a8-b66e-4b21-8b45-0d72b68f4790")
userID := uuid.MustParse("6bf00877-a693-4d77-b28e-8c8292adac98")
cfg, err := config.Load()
if err != nil {
failReport(out, "FAIL", "config: "+err.Error(), nil)
}
ctx := context.Background()
pool, err := db.NewPool(ctx, cfg.DatabaseURL, db.PoolOptions{
MaxConns: int32(cfg.DBMaxConns), MinConns: int32(cfg.DBMinConns),
MaxConnLifetime: cfg.DBMaxConnLifetime, MaxConnLifetimeJitter: cfg.DBMaxConnLifetimeJitter,
MaxConnIdleTime: cfg.DBMaxConnIdleTime, HealthCheckPeriod: cfg.DBHealthCheckPeriod,
StatementTimeout: cfg.DBStatementTimeout,
})
if err != nil {
failReport(out, "FAIL", "db: "+err.Error(), nil)
}
defer pool.Close()
// --- Sync A1 from repo seed only ---
var legacy string
_ = pool.QueryRow(ctx, `SELECT COALESCE(legacy_company_id,'') FROM companies WHERE id=$1`, companyID).Scan(&legacy)
a1Cohort := billing.IsA1CohortCompany(legacy, "A1 Slovenija")
syncRes, serr := processing.SyncCompanyA1(ctx, pool, companyID, "A1 Slovenija", a1Cohort, processing.SyncCompanyA1Opts{
SkipDumpBackfill: true,
FixCompanyCatalogOpts: processing.FixCompanyCatalogOpts{
BackfillCategories: true,
WPCategoriesSQL: wpBytes,
AIPrompts: aiprompts.NewService(pool),
},
})
syncInfo := map[string]any{"wp_path": wpPath, "wp_bytes": len(wpBytes), "error": nil, "result": syncRes}
if serr != nil {
syncInfo["error"] = serr.Error()
writeJSON(out, "sync_quick.json", syncInfo)
failReport(out, "FAIL", "SyncCompanyA1: "+serr.Error(), syncInfo)
}
writeJSON(out, "sync_quick.json", syncInfo)
log.Printf("sync ok prompts_already_ok=%d wp_entries=%d", syncRes.CategoryPromptsAlreadyOK, syncRes.WPCategoriesEntries)
platEnv := platformsettings.EnvConfig{
AppEncryptionKey: cfg.AppEncryptionKey, CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
TokenSigningSecret: cfg.TokenSigningSecret, DatabaseURL: cfg.DatabaseURL,
OpenAIAPIKey: cfg.OpenAIAPIKey, OpenAIBaseURL: cfg.OpenAIBaseURL, OpenAIModel: cfg.OpenAIModel,
}
plat := platformsettings.NewService(pool, platEnv)
aiSvc := aiprovider.NewService(pool, aiprovider.EnvConfig{
AppEncryptionKey: cfg.AppEncryptionKey, CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
TokenSigningSecret: cfg.TokenSigningSecret, DatabaseURL: cfg.DatabaseURL,
OpenAIAPIKey: cfg.OpenAIAPIKey, OpenAIBaseURL: cfg.OpenAIBaseURL, OpenAIModel: cfg.OpenAIModel,
ProcessingRPM: cfg.ProcessingRPM, ProcessingMaxRetries: cfg.ProcessingMaxRetries,
})
aiSvc.Platform = plat
roleCfg, rerr := plat.ResolveAIConfig(ctx, platformsettings.AIRoleProcessing)
if rerr != nil {
failReport(out, "FAIL", "ResolveAIConfig: "+rerr.Error(), nil)
}
completer, modeLabel, byok, cerr := aiSvc.ResolveCompleterForRole(ctx, companyID, aiprovider.RoleProcessing)
if cerr != nil {
failReport(out, "FAIL", "ResolveCompleter: "+cerr.Error(), nil)
}
client, ok := completer.(*processing.OpenAIClient)
if !ok || client == nil {
failReport(out, "FAIL", fmt.Sprintf("completer type %T", completer), nil)
}
base := strings.TrimSpace(client.BaseURL)
model := strings.TrimSpace(client.Model)
resolve := map[string]any{
"role_source": roleCfg.Source, "role_provider": roleCfg.Provider,
"completer_base": base, "completer_model": model,
"mode": modeLabel, "byok": byok, "env_ignored": cfg.OpenAIBaseURL,
}
if processing.IsMockOrLoopbackBaseURL(base) || strings.Contains(base, "18767") {
resolve["fail_reason"] = "mock/loopback refused"
writeJSON(out, "resolve_quick.json", resolve)
failReport(out, "FAIL", "mock/loopback refused — live OverloadedBot only", resolve)
}
// --- /models probe (hard stop on 502; 15s) ---
modelsURL := strings.TrimRight(base, "/") + "/models"
modelsCtx, modelsCancel := context.WithTimeout(ctx, 15*time.Second)
defer modelsCancel()
req, _ := http.NewRequestWithContext(modelsCtx, http.MethodGet, modelsURL, nil)
req.Header.Set("Authorization", "Bearer "+client.APIKey)
req.Header.Set("Accept", "application/json")
t0 := time.Now()
resp, merr := http.DefaultClient.Do(req)
elapsed := time.Since(t0).Round(time.Millisecond)
if merr != nil {
resolve["models_error"] = merr.Error()
writeJSON(out, "resolve_quick.json", resolve)
failReport(out, "FAIL", "GET /models network: "+merr.Error(), resolve)
}
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
_ = resp.Body.Close()
resolve["models_http_status"] = resp.StatusCode
resolve["models_elapsed"] = elapsed.String()
resolve["models_snippet"] = trunc(string(body), 180)
if resp.StatusCode == http.StatusBadGateway {
writeJSON(out, "resolve_quick.json", resolve)
failReport(out, "FAIL", "FAIL 502: GET /v1/models — OverloadedBot unavailable (no mock, no wait)", resolve)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
writeJSON(out, "resolve_quick.json", resolve)
failReport(out, "FAIL", fmt.Sprintf("GET /models HTTP %d", resp.StatusCode), resolve)
}
// Prefer advertised green/code-fast if DB model is bare code-fast (gateway list).
if model == "code-fast" && strings.Contains(string(body), `"green/code-fast"`) {
client.Model = "green/code-fast"
model = client.Model
resolve["completer_model_adjusted"] = model
log.Printf("adjusted model code-fast -> green/code-fast from /models list")
}
// Cap per-call wait (~2.5m) and retries so we never sit on 4m×N.
if client.HTTPClient != nil {
client.HTTPClient.Timeout = 150 * time.Second
}
client.MaxRetries = 1
resolve["client_http_timeout"] = "150s"
resolve["client_max_retries"] = 1
writeJSON(out, "resolve_quick.json", resolve)
log.Printf("LIVE OK models=%d base=%s model=%s", resp.StatusCode, base, model)
pipeline := processing.NewPipeline(pool)
pipeline.BatchSize = cfg.ProcessingBatchSize
pipeline.AI = aiSvc
pipeline.Prompts = aiprompts.NewService(pool)
pipeline.Engine = &processing.Engine{
Completer: nil,
Vector: processing.NoopVectorCategorizer{},
EPREL: eprel.NewClient(eprel.Options{Enabled: true, Timeout: 15 * time.Second}),
ProviderMode: processing.AIProviderInternal,
}
rawIDs := make([]uuid.UUID, 0, len(eans))
for _, ean := range eans {
var id uuid.UUID
if err := pool.QueryRow(ctx, `SELECT id FROM raw_products WHERE company_id=$1 AND gtin=$2`, companyID, ean).Scan(&id); err != nil {
failReport(out, "FAIL", fmt.Sprintf("raw %s: %v", ean, err), resolve)
}
rawIDs = append(rawIDs, id)
}
_, _ = pool.Exec(ctx, `
UPDATE processed_products pp
SET field_sources = COALESCE(field_sources, '{}'::jsonb) - 'enhance_input_hash', updated_at=now()
FROM raw_products rp
WHERE pp.company_id=$1 AND pp.raw_product_id=rp.id AND rp.gtin=ANY($2::text[])`, companyID, eans)
_, _ = pool.Exec(ctx, `
UPDATE processing_jobs SET status='failed', error='yielded to quick final verify', updated_at=now()
WHERE company_id=$1 AND status IN ('pending','running','processing')`, companyID)
jobs, err := pipeline.StartJob(ctx, companyID, userID, rawIDs, "full")
if err != nil || len(jobs) == 0 {
failReport(out, "FAIL", fmt.Sprintf("StartJob: %v", err), resolve)
}
jobID := jobs[0].ID
var dbStatus string
var processedCount int
_ = pool.QueryRow(ctx, `SELECT lower(status), COALESCE(processed_count,0) FROM processing_jobs WHERE id=$1`, jobID).Scan(&dbStatus, &processedCount)
startOK := (dbStatus == "pending" || dbStatus == "queued") && processedCount == 0
startSample := map[string]any{
"process_id": jobID.String(), "status": dbStatus, "processed_items": processedCount,
"total_items": len(rawIDs), "start_ok": startOK,
}
writeJSON(out, "start_sample.json", startSample)
_, _ = pool.Exec(ctx, `UPDATE processing_jobs SET status='running', started_at=COALESCE(started_at,now()), updated_at=now() WHERE id=$1`, jobID)
// Hard wall: 2 products × ~2.53 min ≈ 8 min max
runCtx, runCancel := context.WithTimeout(ctx, 8*time.Minute)
defer runCancel()
procErr := pipeline.ProcessJob(runCtx, jobID)
logBytes, _ := os.ReadFile(logPath)
logText := string(logBytes)
aiEnhance := strings.Contains(logText, "ai_enhance")
items, loadErr := pipeline.LoadV1ProcessJobItems(ctx, companyID, jobID, "full")
if loadErr != nil {
items = nil
}
cards := []map[string]any{}
allPass := startOK && aiEnhance && procErr == nil
for _, item := range items {
ean := str(item["ean"])
if ean == "" {
ean = str(item["product_id"])
}
title := first(str(item["name"]), str(item["title"]))
desc := str(item["description"])
catName := str(item["category_name"])
if catName == "" {
if m, ok := item["category"].(map[string]any); ok {
catName = str(m["name"])
}
}
_, hasMetaT := item["meta_title"]
_, hasMetaD := item["meta_description"]
_, hasID := item["id"]
_, hasPP := item["processed_product_id"]
_, hasRaw := item["raw_product_id"]
hasHTML := strings.Contains(strings.ToLower(desc), "<h2") || strings.Contains(strings.ToLower(desc), "<p>")
checks := map[string]bool{
"name_present": strings.TrimSpace(title) != "",
"category_name_present": strings.TrimSpace(catName) != "",
"no_meta_title": !hasMetaT || item["meta_title"] == nil,
"no_meta_description": !hasMetaD || item["meta_description"] == nil,
"no_id": !hasID,
"no_processed_product_id": !hasPP,
"no_raw_product_id": !hasRaw,
"formula_html_or_empty": hasHTML || strings.TrimSpace(desc) == "" || strings.Contains(strings.ToLower(logText), "empty_or_provider_error") || strings.Contains(strings.ToLower(logText), "refuse"),
}
pass := true
for _, v := range checks {
if !v {
pass = false
break
}
}
if !pass {
allPass = false
}
cards = append(cards, map[string]any{
"ean": ean, "title": title, "category": catName, "desc_len": len(desc),
"has_html": hasHTML, "checks": checks, "pass": pass,
})
}
if procErr != nil {
allPass = false
}
overall := "PASS"
reason := ""
if !allPass {
overall = "FAIL"
if procErr != nil {
reason = "ProcessJob: " + processing.TruncateError(procErr)
} else if !aiEnhance {
reason = "no ai_enhance log lines"
} else if !startOK {
reason = "start semantics not pending/0"
} else {
reason = "scorecard assertion failed"
}
}
report := map[string]any{
"generated_at": time.Now().UTC().Format(time.RFC3339),
"overall": overall,
"blocking_reason": reason,
"sync_pass": true,
"live_llm_pass": true,
"process_run": true,
"job_id": jobID.String(),
"eans": eans,
"start_ok": startOK,
"ai_enhance_seen": aiEnhance,
"process_error": nil,
"scorecards": cards,
"llm": resolve,
"sync": syncInfo,
"note": "slim verify: 2 products, 150s/call, 8m wall; repo seed only",
}
if procErr != nil {
report["process_error"] = processing.TruncateError(procErr)
}
writeJSON(out, "report.json", report)
writeMD(out, report, startSample, cards, logText)
fmt.Printf("OVERALL %s job=%s ai_enhance=%v err=%v\n", overall, jobID, aiEnhance, procErr)
if overall != "PASS" {
os.Exit(1)
}
}
func failReport(out, overall, reason string, extra map[string]any) {
report := map[string]any{
"generated_at": time.Now().UTC().Format(time.RFC3339),
"overall": overall, "blocking_reason": reason,
"process_run": false, "extra": extra,
}
writeJSON(out, "report.json", report)
var b strings.Builder
b.WriteString("# Final verify 2026-08-17 (quick LIVE)\n\n")
b.WriteString(fmt.Sprintf("**Overall: %s**\n\n", overall))
b.WriteString(fmt.Sprintf("Blocking: %s\n\n", reason))
b.WriteString("Live OverloadedBot only. No mock.\n")
_ = os.WriteFile(filepath.Join(out, "REPORT.md"), []byte(b.String()), 0o644)
log.Print(reason)
os.Exit(1)
}
func writeMD(out string, report map[string]any, start map[string]any, cards []map[string]any, logText string) {
var b strings.Builder
b.WriteString("# Final verify 2026-08-17 (quick LIVE)\n\n")
b.WriteString(fmt.Sprintf("**Overall: %v**\n\n", report["overall"]))
if r := str(report["blocking_reason"]); r != "" {
b.WriteString(fmt.Sprintf("Blocking: %s\n\n", r))
}
b.WriteString("## Progress note\n\n")
b.WriteString("Prior hung run killed (4m chat waits on 6 EANs). This pass: live OverloadedBot only, repo seed sync, 2 products, 150s/call, 8m wall.\n\n")
b.WriteString("## 1. Sync A1 (repo seed only)\n\n")
b.WriteString("- Source: `scripts/seed/wp_product_categories.sql` (no upload, SkipDumpBackfill)\n")
b.WriteString("- See `sync_quick.json`\n\n")
b.WriteString("## 2. Live LLM\n\n")
if llm, ok := report["llm"].(map[string]any); ok {
b.WriteString(fmt.Sprintf("- base: `%v`\n- model: `%v`\n- models HTTP: `%v`\n- source: `%v`\n\n",
llm["completer_base"], llm["completer_model"], llm["models_http_status"], llm["role_source"]))
}
b.WriteString("## 3. Start semantics\n\n")
b.WriteString(fmt.Sprintf("- process_id: `%v`\n- status: `%v`\n- processed_items: `%v`\n- start_ok: `%v`\n\n",
start["process_id"], start["status"], start["processed_items"], start["start_ok"]))
b.WriteString(fmt.Sprintf("## 4. ai_enhance\n\n- seen: `%v`\n\n", report["ai_enhance_seen"]))
b.WriteString("## 5. Scorecards\n\n")
for _, c := range cards {
b.WriteString(fmt.Sprintf("- **%v** pass=%v cat=%v html=%v title=%q\n",
c["ean"], c["pass"], c["category"], c["has_html"], trunc(str(c["title"]), 60)))
}
b.WriteString("\n## Log excerpt (ai_enhance)\n\n```\n")
n := 0
for _, line := range strings.Split(logText, "\n") {
if strings.Contains(line, "ai_enhance") || strings.Contains(line, "LIVE") || strings.Contains(line, "OVERALL") {
b.WriteString(trunc(line, 240) + "\n")
n++
if n >= 12 {
break
}
}
}
b.WriteString("```\n")
_ = os.WriteFile(filepath.Join(out, "REPORT.md"), []byte(b.String()), 0o644)
}
func writeJSON(out, name string, v any) {
raw, _ := json.MarshalIndent(v, "", " ")
_ = os.WriteFile(filepath.Join(out, name), raw, 0o644)
}
func str(v any) string {
if v == nil {
return ""
}
if s, ok := v.(string); ok {
return s
}
return strings.TrimSpace(fmt.Sprint(v))
}
func first(vals ...string) string {
for _, v := range vals {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
func trunc(s string, n int) string {
s = strings.TrimSpace(s)
if len(s) <= n {
return s
}
return s[:n] + "…"
}