feat(pdf): rename acquisition-library to pdf-library and update package name to @mintel/pdf
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 13s
Monorepo Pipeline / 🏗️ Build (push) Failing after 11s
Monorepo Pipeline / 🧪 Test (push) Failing after 25s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped

This commit is contained in:
2026-02-12 21:46:45 +01:00
parent 269d19bbef
commit a4d021c658
24 changed files with 378 additions and 9 deletions

View File

@@ -0,0 +1,153 @@
import { CheerioCrawler } from "@crawlee/cheerio";
import axios from "axios";
import { FileCacheAdapter } from "../utils/cache/FileCacheAdapter.js";
import { initialState } from "../logic/pricing/constants.js";
import { FormState } from "../logic/pricing/types.js";
export interface AcquisitionResult {
state: FormState;
usage: {
prompt: number;
completion: number;
cost: number;
};
}
export class AcquisitionService {
private cache: FileCacheAdapter;
private openRouterKey: string;
constructor(openRouterKey: string) {
this.openRouterKey = openRouterKey;
this.cache = new FileCacheAdapter({ prefix: "acq_" });
}
async runFullSequence(url: string, briefing: string, comments?: string): Promise<AcquisitionResult> {
console.log(`🚀 Starting Acquisition Sequence for: ${url}`);
// 1. Crawl
const crawlData = await this.performCrawl(url);
// 2. Distill
const distilledContext = await this.distillCrawlContext(crawlData);
// 3. AI Estimation (using parts of the original ai-estimate logic)
// For brevity in this initial port, I'll implement a combined prompt strategy
// or keep the multi-pass if needed.
const result = await this.getAiEstimation(briefing, distilledContext, comments || null);
return result;
}
private async performCrawl(url: string): Promise<string> {
const pages: any[] = [];
const origin = new URL(url).origin;
const crawler = new CheerioCrawler({
maxRequestsPerCrawl: 15,
async requestHandler({ $, request, enqueueLinks }) {
const title = $("title").text();
const bodyText = $("body").text().replace(/\s+/g, " ").substring(0, 10000);
pages.push({
url: request.url,
content: `Title: ${title}\nText: ${bodyText}`,
});
await enqueueLinks({
limit: 10,
transformRequestFunction: (req) => {
try {
const reqUrl = new URL(req.url);
if (reqUrl.origin !== origin) return false;
if (reqUrl.pathname.match(/\.(pdf|zip|jpg|png|svg|webp)$/i)) return false;
return req;
} catch (_error) {
// Ignored - malformed URL in enqueueLinks
return false;
}
},
});
},
});
await crawler.run([url]);
return pages.map((p) => `--- PAGE: ${p.url} ---\n${p.content}`).join("\n\n");
}
private async distillCrawlContext(rawCrawl: string): Promise<string> {
const systemPrompt = `
You are a context distiller. Extract the "Company DNA" in 5-8 bullet points (GERMAN).
Focus on: Services, USPs, Target Audience, Tone.
`;
const resp = await axios.post(
"https://openrouter.ai/api/v1/chat/completions",
{
model: "google/gemini-3-flash-preview",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: `RAW_CRAWL:\n${rawCrawl.substring(0, 20000)}` },
],
},
{
headers: { Authorization: `Bearer ${this.openRouterKey}` },
}
);
return resp.data.choices[0].message.content;
}
private async getAiEstimation(briefing: string, context: string, comments: string | null): Promise<AcquisitionResult> {
// Porting a simplified version of Pass 1 and Pass 3 together for the "Audit"
const systemPrompt = `
You are a Digital Architect. Analyze the briefing and crawl context.
Generate a JSON state for a project estimation.
Language: GERMAN.
Format: ROOT LEVEL JSON matching FormState interface.
### PRICING RULES:
- Base: 5440 €
- Page: 600 €
- Feature: 1500 €
- Function/API: 800 €
Return ONLY the JSON.
`;
const userPrompt = `BRIEFING: ${briefing}\n\nCONTEXT: ${context}\n\nCOMMENTS: ${comments}`;
const resp = await axios.post(
"https://openrouter.ai/api/v1/chat/completions",
{
model: "google/gemini-3-flash-preview",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
response_format: { type: "json_object" },
},
{
headers: { Authorization: `Bearer ${this.openRouterKey}` },
}
);
let state: FormState;
try {
state = JSON.parse(resp.data.choices[0].message.content);
} catch (_error) {
console.error("Failed to parse AI estimation JSON, returning initial state.");
state = initialState;
}
// Ensure it matches FormState defaults
const finalState = { ...initialState, ...state };
return {
state: finalState,
usage: {
prompt: resp.data.usage?.prompt_tokens || 0,
completion: resp.data.usage?.completion_tokens || 0,
cost: resp.data.usage?.cost || 0,
},
};
}
}

View File

@@ -0,0 +1,24 @@
import { renderToFile } from "@react-pdf/renderer";
import { createElement } from "react";
import { EstimationPDF } from "../components/EstimationPDF.js";
import { PRICING } from "../logic/pricing/constants.js";
import { calculateTotals } from "../logic/pricing/calculator.js";
export class PdfEngine {
constructor() { }
async generateEstimatePdf(state: any, outputPath: string): Promise<string> {
const totals = calculateTotals(state, PRICING);
await renderToFile(
createElement(EstimationPDF as any, {
state,
totalPrice: totals.totalPrice,
pricing: PRICING,
} as any) as any,
outputPath
);
return outputPath;
}
}