ai-inference-stepper 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +169 -0
- package/.eslintrc.cjs +23 -0
- package/.github/workflows/ci.yml +51 -0
- package/.github/workflows/keep-alive.yml +22 -0
- package/.github/workflows/publish.yml +34 -0
- package/ARCHITECTURE.md +594 -0
- package/Dockerfile +16 -0
- package/LICENSE +28 -0
- package/README.md +261 -0
- package/dist/alerts/discord.d.ts +19 -0
- package/dist/alerts/discord.d.ts.map +1 -0
- package/dist/alerts/discord.js +70 -0
- package/dist/alerts/discord.js.map +1 -0
- package/dist/cache/redisCache.d.ts +45 -0
- package/dist/cache/redisCache.d.ts.map +1 -0
- package/dist/cache/redisCache.js +171 -0
- package/dist/cache/redisCache.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +251 -0
- package/dist/config.js.map +1 -0
- package/dist/fallback/templateFallback.d.ts +7 -0
- package/dist/fallback/templateFallback.d.ts.map +1 -0
- package/dist/fallback/templateFallback.js +29 -0
- package/dist/fallback/templateFallback.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +198 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.d.ts +10 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +44 -0
- package/dist/logging.js.map +1 -0
- package/dist/metrics/metrics.d.ts +22 -0
- package/dist/metrics/metrics.d.ts.map +1 -0
- package/dist/metrics/metrics.js +78 -0
- package/dist/metrics/metrics.js.map +1 -0
- package/dist/providers/factory.d.ts +11 -0
- package/dist/providers/factory.d.ts.map +1 -0
- package/dist/providers/factory.js +52 -0
- package/dist/providers/factory.js.map +1 -0
- package/dist/providers/hfSpace.adapter.d.ts +21 -0
- package/dist/providers/hfSpace.adapter.d.ts.map +1 -0
- package/dist/providers/hfSpace.adapter.js +110 -0
- package/dist/providers/hfSpace.adapter.js.map +1 -0
- package/dist/providers/httpTemplate.adapter.d.ts +42 -0
- package/dist/providers/httpTemplate.adapter.d.ts.map +1 -0
- package/dist/providers/httpTemplate.adapter.js +98 -0
- package/dist/providers/httpTemplate.adapter.js.map +1 -0
- package/dist/providers/promptBuilder.d.ts +34 -0
- package/dist/providers/promptBuilder.d.ts.map +1 -0
- package/dist/providers/promptBuilder.js +315 -0
- package/dist/providers/promptBuilder.js.map +1 -0
- package/dist/providers/provider.interface.d.ts +45 -0
- package/dist/providers/provider.interface.d.ts.map +1 -0
- package/dist/providers/provider.interface.js +47 -0
- package/dist/providers/provider.interface.js.map +1 -0
- package/dist/providers/specs.d.ts +18 -0
- package/dist/providers/specs.d.ts.map +1 -0
- package/dist/providers/specs.js +326 -0
- package/dist/providers/specs.js.map +1 -0
- package/dist/providers/unified.adapter.d.ts +37 -0
- package/dist/providers/unified.adapter.d.ts.map +1 -0
- package/dist/providers/unified.adapter.js +141 -0
- package/dist/providers/unified.adapter.js.map +1 -0
- package/dist/queue/producer.d.ts +30 -0
- package/dist/queue/producer.d.ts.map +1 -0
- package/dist/queue/producer.js +87 -0
- package/dist/queue/producer.js.map +1 -0
- package/dist/queue/worker.d.ts +9 -0
- package/dist/queue/worker.d.ts.map +1 -0
- package/dist/queue/worker.js +137 -0
- package/dist/queue/worker.js.map +1 -0
- package/dist/server/app.d.ts +4 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +394 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/start.d.ts +16 -0
- package/dist/server/start.d.ts.map +1 -0
- package/dist/server/start.js +45 -0
- package/dist/server/start.js.map +1 -0
- package/dist/stepper/orchestrator.d.ts +22 -0
- package/dist/stepper/orchestrator.d.ts.map +1 -0
- package/dist/stepper/orchestrator.js +333 -0
- package/dist/stepper/orchestrator.js.map +1 -0
- package/dist/types.d.ts +216 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/redaction.d.ts +9 -0
- package/dist/utils/redaction.d.ts.map +1 -0
- package/dist/utils/redaction.js +41 -0
- package/dist/utils/redaction.js.map +1 -0
- package/dist/utils/safeRequest.d.ts +38 -0
- package/dist/utils/safeRequest.d.ts.map +1 -0
- package/dist/utils/safeRequest.js +104 -0
- package/dist/utils/safeRequest.js.map +1 -0
- package/dist/validation/report.schema.d.ts +48 -0
- package/dist/validation/report.schema.d.ts.map +1 -0
- package/dist/validation/report.schema.js +72 -0
- package/dist/validation/report.schema.js.map +1 -0
- package/dist/webhooks/delivery.d.ts +31 -0
- package/dist/webhooks/delivery.d.ts.map +1 -0
- package/dist/webhooks/delivery.js +102 -0
- package/dist/webhooks/delivery.js.map +1 -0
- package/docs/assets/architecture.png +0 -0
- package/package.json +75 -0
- package/render.yaml +25 -0
- package/src/alerts/README.md +25 -0
- package/src/alerts/discord.ts +86 -0
- package/src/cache/How redis caching works in package stepper.md +971 -0
- package/src/cache/README.md +51 -0
- package/src/cache/redisCache.ts +194 -0
- package/src/ci/deploy.sh +36 -0
- package/src/cli.ts +9 -0
- package/src/config.ts +265 -0
- package/src/fallback/templateFallback.ts +32 -0
- package/src/index.ts +246 -0
- package/src/logging.ts +46 -0
- package/src/metrics/README.md +24 -0
- package/src/metrics/metrics.ts +84 -0
- package/src/providers/How the providers interact.md +121 -0
- package/src/providers/README.md +121 -0
- package/src/providers/factory.ts +57 -0
- package/src/providers/hfSpace.adapter.ts +119 -0
- package/src/providers/httpTemplate.adapter.ts +138 -0
- package/src/providers/promptBuilder.ts +330 -0
- package/src/providers/provider.interface.ts +73 -0
- package/src/providers/specs.ts +366 -0
- package/src/providers/unified.adapter.ts +172 -0
- package/src/queue/How queue works in package stepper.md +149 -0
- package/src/queue/README.md +41 -0
- package/src/queue/producer.ts +108 -0
- package/src/queue/worker.ts +170 -0
- package/src/server/app.ts +451 -0
- package/src/server/start.ts +68 -0
- package/src/stepper/Dockerfile +48 -0
- package/src/stepper/How orchestrator works in package stepper.md +746 -0
- package/src/stepper/README.md +43 -0
- package/src/stepper/orchestrator.ts +437 -0
- package/src/types.ts +238 -0
- package/src/utils/redaction.ts +50 -0
- package/src/utils/safeRequest.ts +140 -0
- package/src/validation/README.md +25 -0
- package/src/validation/report.schema.ts +96 -0
- package/src/webhooks/delivery.ts +162 -0
- package/tests/integration/full-flow.test.ts +192 -0
- package/tests/unit/alerts/discord.test.ts +119 -0
- package/tests/unit/cache.test.ts +87 -0
- package/tests/unit/orchestrator-fallback.test.ts +92 -0
- package/tests/unit/orchestrator.test.ts +105 -0
- package/tests/unit/providers/factory.test.ts +161 -0
- package/tests/unit/providers/unified.adapter.test.ts +206 -0
- package/tests/unit/utils/redaction.test.ts +140 -0
- package/tests/unit/utils/safeRequest.test.ts +164 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Chat Conversation
|
|
2
|
+
|
|
3
|
+
Note: _This is purely the output of the chat conversation and does not contain any raw data, codebase snippets, etc. used to generate the output._
|
|
4
|
+
|
|
5
|
+
### User Input
|
|
6
|
+
|
|
7
|
+
lets break down the explanation of @[packages/stepper/src/queue/producer.ts]
|
|
8
|
+
. i want to know what each functions means, how they work, possible results of it, why they work like that and how they help achieve what the inference stepper package is for.
|
|
9
|
+
break down evey detail and explanton for me, to the lowest and clear enough for even a non programmer or technial person
|
|
10
|
+
|
|
11
|
+
*Viewed [producer.ts](file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts) *
|
|
12
|
+
|
|
13
|
+
*Viewed [index.ts](file:///home/blaze/mine/commitdiary/packages/stepper/src/index.ts) *
|
|
14
|
+
|
|
15
|
+
### Planner Response
|
|
16
|
+
|
|
17
|
+
This file acts as the **"Dispatcher"** or **"Order Taker"** for the system.
|
|
18
|
+
|
|
19
|
+
Imagine a busy coffee shop. Making a complex latte takes time. If the cashier made the coffee for every customer right then and there, the line would never move.
|
|
20
|
+
Instead, the cashier (Producer) takes the order, writes it on a cup, and places it in a line (Queue) for the baristas (Workers) to make. The customer gets a receipt number to check when it's ready.
|
|
21
|
+
|
|
22
|
+
This [producer.ts](cci:7://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts:0:0-0:0) file is exactly that **Cashier**. It takes requests to generate reports and puts them in a line so the main application stays fast and doesn't freeze while waiting for the work to be done.
|
|
23
|
+
|
|
24
|
+
Here is the breakdown of every part:
|
|
25
|
+
|
|
26
|
+
### 1. [getQueue()](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts:11:0-25:1)
|
|
27
|
+
**The "Connection Starter"**
|
|
28
|
+
|
|
29
|
+
* **What it does:** This function checks if we are already connected to the "Job Line" (the Queue system). If we are, it just returns that connection. If not, it creates a new connection to the system (Redis/BullMQ).
|
|
30
|
+
* **Why it works like this:** Opening a new connection every single time you want to add a job is slow and wasteful (like buying a new phone every time you want to make a call). Ideally, you want to keep one phone line open and re-use it.
|
|
31
|
+
* **Result:** A working link to the queue system, ready to accept jobs.
|
|
32
|
+
|
|
33
|
+
### 2. [enqueueReportJob(input, cacheKey, options)](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts:27:0-60:1)
|
|
34
|
+
**The "Order Taker"**
|
|
35
|
+
|
|
36
|
+
* **What it does:** This is the most important function. It takes the "Order" (the commit details, user ID, etc.), creates a unique "Ticket Number" (Job ID), and places the order into the queue.
|
|
37
|
+
* **How it works:**
|
|
38
|
+
1. It generates a unique ID (like `uuidv4`) so we can track this specific job later.
|
|
39
|
+
2. It packages all the data (userId, commit info) into a neat little bundle (`jobData`).
|
|
40
|
+
3. It pushes this bundle onto the queue with instructions (like "if this fails, keep the record for a bit so we can see why").
|
|
41
|
+
* **Why it works like this:** By "enqueueing" (putting in line) the job instead of doing it immediately, the function returns the `Job ID` **instantly**. This means the user's browser isn't stuck loading for 20 seconds while the report generates. It feels instant.
|
|
42
|
+
* **Result:** A returned `Job ID` string (e.g., `"1234-5678..."`) that serves as a receipt.
|
|
43
|
+
|
|
44
|
+
### 3. [getJobStatus(jobId)](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts:62:0-92:1)
|
|
45
|
+
**The "Status Board"**
|
|
46
|
+
|
|
47
|
+
* **What it does:** This allows us to check on a specific order using the "Ticket Number" (Job ID) we got earlier.
|
|
48
|
+
* **How it works:** It asks the queue system: "Hey, what is happening with Job #123?".
|
|
49
|
+
* **Possible Results:**
|
|
50
|
+
* **"waiting"**: Still in line.
|
|
51
|
+
* **"active"**: Currently being worked on.
|
|
52
|
+
* **"completed"**: Done! Here is the result (the report).
|
|
53
|
+
* **"failed"**: Something went wrong (and here is the error reason).
|
|
54
|
+
* **Why it works like this:** Since the background worker is doing the job separately, we need a way to "peek" at its progress without interrupting it.
|
|
55
|
+
|
|
56
|
+
### 4. [closeQueue()](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts:94:0-103:1)
|
|
57
|
+
**The "Shutdown Switch"**
|
|
58
|
+
|
|
59
|
+
* **What it does:** Safely disconnects from the queue system.
|
|
60
|
+
* **Why it works like this:** When the application needs to restart or shut down, you don't want to just "pull the plug." This function politely closes the connection so no data is corrupted or lost.
|
|
61
|
+
* **Result:** The connection variable is reset to `null`.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### **Summary: How this helps the Inference Stepper**
|
|
66
|
+
|
|
67
|
+
The Inference Stepper is designed to use AI to analyze code, which is **slow** (it can take 5-30 seconds).
|
|
68
|
+
|
|
69
|
+
Without this file:
|
|
70
|
+
> User clicks "Generate" -> **Wait 30 seconds...** -> Screen freezes -> Finally sees result.
|
|
71
|
+
|
|
72
|
+
With this file:
|
|
73
|
+
> User clicks "Generate" -> **Instant response** ("We're working on it! Order #123") -> The user can continue doing other things -> The system checks [getJobStatus](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts:62:0-92:1) periodically -> Pop! The result appears when ready.
|
|
74
|
+
|
|
75
|
+
This file ensures the system feels **premium, fast, and responsive**, even though the work being done behind the scenes is heavy and slow.
|
|
76
|
+
|
|
77
|
+
### User Input
|
|
78
|
+
|
|
79
|
+
lets break down the explanation of
|
|
80
|
+
. i want to know what each functions means, how they work, possible results of it, why they work like that and how they help achieve what the inference stepper package is for.
|
|
81
|
+
break down evey detail and explanton for me, to the lowest and clear enough for even a non programmer or technial person
|
|
82
|
+
|
|
83
|
+
### User Input
|
|
84
|
+
|
|
85
|
+
lets break down the explanation of @[packages/stepper/src/queue/worker.ts]
|
|
86
|
+
. i want to know what each functions means, how they work, possible results of it, why they work like that and how they help achieve what the inference stepper package is for.
|
|
87
|
+
break down evey detail and explanton for me, to the lowest and clear enough for even a non programmer or technial person
|
|
88
|
+
|
|
89
|
+
*Viewed [worker.ts](file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/worker.ts) *
|
|
90
|
+
|
|
91
|
+
### Planner Response
|
|
92
|
+
|
|
93
|
+
This file acts as the **"Kitchen Staff"** or the **"Barista"** of the system.
|
|
94
|
+
|
|
95
|
+
If the [producer.ts](cci:7://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts:0:0-0:0) we discussed earlier is the **Cashier** taking orders and putting them on a line, then this [worker.ts](cci:7://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/worker.ts:0:0-0:0) is the **Worker** who stands at the back, picks up those orders one by one, and actually does the hard work of "brewing the coffee."
|
|
96
|
+
|
|
97
|
+
Here is the breakdown of every part:
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### 1. [processReportJob(job)](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/worker.ts:12:0-56:1)
|
|
102
|
+
**The "Cooking Instructions"**
|
|
103
|
+
|
|
104
|
+
* **What it does:** This is the core logic. It tells the worker exactly *how* to handle an order once it picks it up from the line.
|
|
105
|
+
* **How it works:**
|
|
106
|
+
1. **Check the "Fridge" (Cache):** First, it checks if someone else already made this specific report while it was waiting. If it's already done, it just stops to save time.
|
|
107
|
+
2. **Do the Hard Work:** It calls `generateReportNow`, which is the part that actually talks to the AI and asks it to analyze the code.
|
|
108
|
+
3. **Save the Result:** Once the AI finishes, it saves that result into the "Fridge" (Cache) so that when the user checks their "Receipt" (Job ID), the info is ready for them.
|
|
109
|
+
4. **Mark as Done:** It updates the status to "100% complete."
|
|
110
|
+
* **Why it works like this:** It runs "asynchronously." This means it doesn't matter if it takes 5 seconds or 30 seconds; it's happening in the background without slowing down the rest of the website.
|
|
111
|
+
* **Possible Results:**
|
|
112
|
+
* **Success:** The report is generated and saved.
|
|
113
|
+
* **Failure:** If the AI is down or something breaks, it records the error so the user knows *why* it failed.
|
|
114
|
+
|
|
115
|
+
### 2. [startWorker()](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/worker.ts:58:0-89:1)
|
|
116
|
+
**The "Opening the Kitchen"**
|
|
117
|
+
|
|
118
|
+
* **What it does:** This is the button that turns the worker "ON."
|
|
119
|
+
* **How it works:**
|
|
120
|
+
1. It checks if a worker is already running (no need to hire two people for the same spot).
|
|
121
|
+
2. It connects to the same "Order Line" (the Queue) as the Cashier.
|
|
122
|
+
3. It sets the **Concurrency** (how many jobs it can do at once). If concurrency is `5`, it’s like having 5 baristas working simultaneously.
|
|
123
|
+
4. It sets up "Listeners" (Ears) to hear when a job finishes or fails, so it can write it down in the logs for the developers to see.
|
|
124
|
+
* **Why it works like this:** By separating the [startWorker](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/worker.ts:58:0-89:1) function, we can choose *where* to run the worker. We could have 10 different computers all running this worker to handle thousands of users at once.
|
|
125
|
+
* **Result:** The system starts listening for new orders and begins processing them immediately.
|
|
126
|
+
|
|
127
|
+
### 3. [stopWorker()](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/worker.ts:91:0-100:1)
|
|
128
|
+
**The "Closing Time"**
|
|
129
|
+
|
|
130
|
+
* **What it does:** Politely tells the worker to stop taking new orders and finish what they are doing before going home.
|
|
131
|
+
* **How it works:** It closes the connection to the queue system.
|
|
132
|
+
* **Why it works like this:** If you just "killed" the computer while it was halfway through a report, the report would be lost forever. [stopWorker](cci:1://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/worker.ts:91:0-100:1) ensures a "Graceful Shutdown"—finishing the current task before turning off the lights.
|
|
133
|
+
* **Result:** The worker shuts down safely.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### **How this helps the Inference Stepper**
|
|
138
|
+
|
|
139
|
+
The **Inference Stepper** package is meant to be a high-performance tool for generating AI-powered commit messages or reports.
|
|
140
|
+
|
|
141
|
+
Without this [worker.ts](cci:7://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/worker.ts:0:0-0:0):
|
|
142
|
+
* The system would have a list of orders (from the [producer.ts](cci:7://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts:0:0-0:0)) but **no one to actually do the work.**
|
|
143
|
+
* The AI analysis would never happen, and every order would just sit in line forever.
|
|
144
|
+
|
|
145
|
+
**In simple terms:**
|
|
146
|
+
* [producer.ts](cci:7://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/producer.ts:0:0-0:0) says: "Someone wants a report!"
|
|
147
|
+
* [worker.ts](cci:7://file:///home/blaze/mine/commitdiary/packages/stepper/src/queue/worker.ts:0:0-0:0) says: "I'm on it. I'll ask the AI, save the answer, and let everyone know when I'm done."
|
|
148
|
+
|
|
149
|
+
By having this background worker, the **Inference Stepper** can handle hundreds of requests at the same time without ever making the user’s screen feel "laggy" or "frozen." It turns a slow AI process into a smooth, professional-feeling experience.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 📥 Async Job Queue
|
|
2
|
+
|
|
3
|
+
AI generation is slow (it can take 30-60 seconds). To keep the application responsive, the **Inference Stepper** uses a background job queue powered by **BullMQ** and **Redis**.
|
|
4
|
+
|
|
5
|
+
## 🎯 Purpose
|
|
6
|
+
|
|
7
|
+
- **Responsiveness**: Users get an immediate "Job Enqueued" response instead of waiting for the AI.
|
|
8
|
+
- **Reliability**: If a job fails due to a temporary glitch, it can be retried automatically.
|
|
9
|
+
- **Concurrency Control**: We can limit how many AI requests run at the same time to avoid hitting global rate limits.
|
|
10
|
+
|
|
11
|
+
## 👥 Major Organs
|
|
12
|
+
|
|
13
|
+
### 1. Producer (`producer.ts`)
|
|
14
|
+
|
|
15
|
+
The "Cashier" of the system.
|
|
16
|
+
|
|
17
|
+
- Takes the incoming request.
|
|
18
|
+
- Assigns a unique `jobId`.
|
|
19
|
+
- Puts the task into the Redis queue.
|
|
20
|
+
- Returns the `jobId` to the caller.
|
|
21
|
+
|
|
22
|
+
### 2. Worker (`worker.ts`)
|
|
23
|
+
|
|
24
|
+
The "Kitchen Staff" who does the actual work.
|
|
25
|
+
|
|
26
|
+
- Sits in the background and waits for jobs.
|
|
27
|
+
- Picks up a job when one is available.
|
|
28
|
+
- Calls the **Orchestrator** to do the AI inference.
|
|
29
|
+
- Updates the cache with the final result.
|
|
30
|
+
|
|
31
|
+
## 📋 Functions
|
|
32
|
+
|
|
33
|
+
| Function | Role |
|
|
34
|
+
| -------------------- | ----------------------------------------------------------------- |
|
|
35
|
+
| `enqueueReportJob()` | Adds a new report request to the queue. |
|
|
36
|
+
| `getJobStatus()` | Checks if a job is `waiting`, `active`, `completed`, or `failed`. |
|
|
37
|
+
| `processReportJob()` | The internal logic the worker runs for every ticket. |
|
|
38
|
+
|
|
39
|
+
## 🚀 Scaling
|
|
40
|
+
|
|
41
|
+
This architecture allows for **horizontal scaling**. You can run one instance of the API server (Producer) and ten instances of the Worker on different machines to handle high traffic, all sharing the same Redis queue.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// packages/stepper/src/queue/producer.ts`
|
|
2
|
+
|
|
3
|
+
import { Queue } from 'bullmq';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import { getRedisClient } from '../cache/redisCache.js';
|
|
6
|
+
import { PromptInput, ReportJobData } from '../types.js';
|
|
7
|
+
import { config } from '../config.js';
|
|
8
|
+
import { logger } from '../logging.js';
|
|
9
|
+
|
|
10
|
+
let queue: Queue<ReportJobData> | null = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get or create BullMQ queue
|
|
14
|
+
*/
|
|
15
|
+
export function getQueue(): Queue<ReportJobData> {
|
|
16
|
+
if (!queue) {
|
|
17
|
+
const connection = getRedisClient();
|
|
18
|
+
queue = new Queue<ReportJobData>(config.queue.name, { connection });
|
|
19
|
+
|
|
20
|
+
logger.info({ queueName: config.queue.name }, 'BullMQ queue initialized');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return queue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Enqueue a report generation job
|
|
28
|
+
* The "Order Taker"
|
|
29
|
+
*/
|
|
30
|
+
export async function enqueueReportJob(
|
|
31
|
+
input: PromptInput,
|
|
32
|
+
cacheKey: string,
|
|
33
|
+
options: { priority?: number; callbackUrl?: string } = {}
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const jobId = uuidv4();
|
|
36
|
+
const queue = getQueue();
|
|
37
|
+
|
|
38
|
+
const jobData: ReportJobData = {
|
|
39
|
+
jobId,
|
|
40
|
+
input,
|
|
41
|
+
cacheKey,
|
|
42
|
+
priority: options.priority,
|
|
43
|
+
callbackUrl: options.callbackUrl,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await queue.add('generate-report', jobData, {
|
|
48
|
+
jobId,
|
|
49
|
+
priority: options.priority,
|
|
50
|
+
removeOnComplete: 100, // Keep last 100 completed jobs
|
|
51
|
+
removeOnFail: 500, // Keep last 500 failed jobs
|
|
52
|
+
attempts: 5, // Retry up to 5 times if all providers fail
|
|
53
|
+
backoff: {
|
|
54
|
+
type: 'exponential',
|
|
55
|
+
delay: 10000, // Start with 10 seconds, doubles each retry
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
logger.info({ jobId, userId: input.userId, commitSha: input.commitSha }, 'Job enqueued');
|
|
60
|
+
return jobId;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.error({ error, jobId }, 'Failed to enqueue job');
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get job status
|
|
69
|
+
*/
|
|
70
|
+
export async function getJobStatus(jobId: string): Promise<{
|
|
71
|
+
id: string;
|
|
72
|
+
state: string;
|
|
73
|
+
progress?: number;
|
|
74
|
+
result?: unknown;
|
|
75
|
+
failedReason?: string;
|
|
76
|
+
data?: unknown;
|
|
77
|
+
} | null> {
|
|
78
|
+
const queue = getQueue();
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const job = await queue.getJob(jobId);
|
|
82
|
+
if (!job) return null;
|
|
83
|
+
|
|
84
|
+
const state = await job.getState();
|
|
85
|
+
return {
|
|
86
|
+
id: job.id!,
|
|
87
|
+
state,
|
|
88
|
+
progress: job.progress as number | undefined,
|
|
89
|
+
result: job.returnvalue,
|
|
90
|
+
failedReason: job.failedReason,
|
|
91
|
+
data: job.data,
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.error({ error, jobId }, 'Failed to get job status');
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Close queue connection (for graceful shutdown)
|
|
101
|
+
*/
|
|
102
|
+
export async function closeQueue(): Promise<void> {
|
|
103
|
+
if (queue) {
|
|
104
|
+
await queue.close();
|
|
105
|
+
queue = null;
|
|
106
|
+
logger.info('Queue closed');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// packages/stepper/src/queue/worker.ts`
|
|
2
|
+
|
|
3
|
+
import { Worker, Job } from 'bullmq';
|
|
4
|
+
import { getRedisClient, setHydrated, markFailed, getReportCache } from '../cache/redisCache.js';
|
|
5
|
+
import { generateReportNow } from '../stepper/orchestrator.js';
|
|
6
|
+
import { ReportJobData } from '../types.js';
|
|
7
|
+
import { config } from '../config.js';
|
|
8
|
+
import { logger, createChildLogger } from '../logging.js';
|
|
9
|
+
import { recordJobProcessed, recordJobFailed } from '../metrics/metrics.js';
|
|
10
|
+
import { sendDiscordAlert } from '../alerts/discord.js';
|
|
11
|
+
import { notifyWebhookSuccess, notifyWebhookFailure } from '../webhooks/delivery.js';
|
|
12
|
+
|
|
13
|
+
let worker: Worker<ReportJobData> | null = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Job processor function
|
|
17
|
+
*/
|
|
18
|
+
async function processReportJob(job: Job<ReportJobData>): Promise<void> {
|
|
19
|
+
const { jobId, input, cacheKey } = job.data;
|
|
20
|
+
const log = createChildLogger({ jobId, userId: input.userId, commitSha: input.commitSha });
|
|
21
|
+
|
|
22
|
+
log.info('Processing report job');
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Check cache again (avoid race condition)
|
|
26
|
+
const cached = await getReportCache(cacheKey);
|
|
27
|
+
if (cached && cached.status === 'hydrated') {
|
|
28
|
+
log.info('Report already hydrated in cache, skipping generation');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Generate report
|
|
33
|
+
const result = await generateReportNow(input, jobId);
|
|
34
|
+
|
|
35
|
+
// Store in cache
|
|
36
|
+
await setHydrated(cacheKey, result.result, result.providersAttempted, result.fallback);
|
|
37
|
+
|
|
38
|
+
// Update job progress
|
|
39
|
+
await job.updateProgress(100);
|
|
40
|
+
|
|
41
|
+
recordJobProcessed();
|
|
42
|
+
log.info({ usedProvider: result.usedProvider, fallback: result.fallback }, 'Job completed successfully');
|
|
43
|
+
|
|
44
|
+
// Note: input.callbacks are already executed in orchestrator immediately after generation
|
|
45
|
+
// The callbackUrl below is the legacy webhook for backwards compatibility
|
|
46
|
+
if (job.data.callbackUrl && config.webhook.enabled) {
|
|
47
|
+
log.info({ callbackUrl: job.data.callbackUrl }, 'Sending success webhook');
|
|
48
|
+
await notifyWebhookSuccess(
|
|
49
|
+
job.data.callbackUrl,
|
|
50
|
+
config.webhook.secret,
|
|
51
|
+
jobId,
|
|
52
|
+
result.result
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
57
|
+
log.error({ error: errorMessage }, 'Job failed');
|
|
58
|
+
|
|
59
|
+
// Mark cache as failed
|
|
60
|
+
await markFailed(cacheKey, errorMessage, []);
|
|
61
|
+
|
|
62
|
+
recordJobFailed();
|
|
63
|
+
|
|
64
|
+
// Execute failure callbacks if configured
|
|
65
|
+
// Note: We handle this directly here since orchestrator threw before callbacks could execute
|
|
66
|
+
// The callbacks are fire-and-forget to not delay the job failure handling
|
|
67
|
+
if (input.callbacks && input.callbacks.length > 0) {
|
|
68
|
+
const failurePayload = {
|
|
69
|
+
success: false,
|
|
70
|
+
error: errorMessage,
|
|
71
|
+
metadata: {
|
|
72
|
+
jobId,
|
|
73
|
+
userId: input.userId,
|
|
74
|
+
commitSha: input.commitSha,
|
|
75
|
+
repo: input.repo,
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (const callback of input.callbacks) {
|
|
81
|
+
fetch(callback.url, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: {
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
'User-Agent': 'Stepper/1.0',
|
|
86
|
+
'X-Stepper-Timestamp': Date.now().toString(),
|
|
87
|
+
...callback.headers,
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify(failurePayload),
|
|
90
|
+
signal: AbortSignal.timeout(10000),
|
|
91
|
+
}).catch((err: Error) => {
|
|
92
|
+
log.warn({ url: callback.url, error: err.message }, 'Failed to send failure callback');
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Send failure webhook notification if configured (legacy)
|
|
98
|
+
if (job.data.callbackUrl && config.webhook.enabled) {
|
|
99
|
+
log.info({ callbackUrl: job.data.callbackUrl }, 'Sending failure webhook');
|
|
100
|
+
await notifyWebhookFailure(
|
|
101
|
+
job.data.callbackUrl,
|
|
102
|
+
config.webhook.secret,
|
|
103
|
+
jobId,
|
|
104
|
+
errorMessage
|
|
105
|
+
).catch(err => {
|
|
106
|
+
log.warn({ error: err.message }, 'Failed to send failure webhook');
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
throw error; // Let BullMQ handle retry logic
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Start worker
|
|
116
|
+
*/
|
|
117
|
+
export function startWorker(): void {
|
|
118
|
+
if (worker) {
|
|
119
|
+
logger.warn('Worker already started');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const connection = getRedisClient();
|
|
124
|
+
|
|
125
|
+
worker = new Worker<ReportJobData>(config.queue.name, processReportJob, {
|
|
126
|
+
connection,
|
|
127
|
+
concurrency: config.queue.concurrency, //(how many jobs it can do at once).
|
|
128
|
+
removeOnComplete: { count: 100 },
|
|
129
|
+
removeOnFail: { count: 500 },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
worker.on('completed', (job) => {
|
|
133
|
+
logger.info({ jobId: job.id }, 'Job completed');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
worker.on('failed', (job, err) => {
|
|
137
|
+
logger.error({ jobId: job?.id, error: err.message }, 'Job failed');
|
|
138
|
+
if (job) {
|
|
139
|
+
void sendDiscordAlert({
|
|
140
|
+
title: 'Job Failed Permanently',
|
|
141
|
+
message: `Job **${job.id}** failed after all retries.\n\n**Error:**\n\`${err.message}\``,
|
|
142
|
+
severity: 'warning',
|
|
143
|
+
metadata: { jobId: job.id, error: err.message, userId: job.data.input.userId }
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
worker.on('error', (err) => {
|
|
149
|
+
logger.error({ error: err }, 'Worker error');
|
|
150
|
+
void sendDiscordAlert({
|
|
151
|
+
title: 'Worker System Error',
|
|
152
|
+
message: `The job queue worker encountered a system error: ${err.message}`,
|
|
153
|
+
severity: 'critical',
|
|
154
|
+
metadata: { error: err.message }
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
logger.info({ concurrency: config.queue.concurrency }, 'Worker started');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Stop worker gracefully
|
|
163
|
+
*/
|
|
164
|
+
export async function stopWorker(): Promise<void> {
|
|
165
|
+
if (worker) {
|
|
166
|
+
await worker.close();
|
|
167
|
+
worker = null;
|
|
168
|
+
logger.info('Worker stopped');
|
|
169
|
+
}
|
|
170
|
+
}
|