groove-dev 0.27.142 → 0.27.144

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.
Files changed (187) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
  4. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
  7. package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
  9. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  10. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  11. package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
  12. package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
  13. package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
  14. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
  15. package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
  16. package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
  17. package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
  18. package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
  19. package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
  20. package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
  21. package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
  22. package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
  23. package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
  24. package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
  25. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
  26. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
  27. package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
  28. package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
  29. package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  31. package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  32. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  33. package/node_modules/@groove-dev/gui/package.json +1 -1
  34. package/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
  35. package/node_modules/@groove-dev/gui/src/app.css +35 -0
  36. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
  37. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
  38. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
  39. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
  40. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
  41. package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
  43. package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
  44. package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  49. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
  50. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
  51. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  52. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  53. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
  54. package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
  55. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
  56. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
  57. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
  58. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
  59. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
  60. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
  61. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
  62. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
  63. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
  64. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
  65. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
  66. package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
  67. package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
  68. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
  69. package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
  70. package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
  71. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
  72. package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
  73. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
  74. package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
  75. package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
  76. package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
  77. package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
  78. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
  79. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
  80. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
  81. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
  82. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
  84. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
  85. package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
  86. package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
  88. package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
  89. package/package.json +1 -1
  90. package/packages/cli/package.json +1 -1
  91. package/packages/daemon/package.json +1 -1
  92. package/packages/daemon/src/api.js +1086 -6532
  93. package/packages/daemon/src/gateways/manager.js +35 -1
  94. package/packages/daemon/src/index.js +3 -0
  95. package/packages/daemon/src/journalist.js +23 -13
  96. package/packages/daemon/src/mlx-server.js +365 -0
  97. package/packages/daemon/src/model-lab.js +308 -12
  98. package/packages/daemon/src/pm.js +1 -1
  99. package/packages/daemon/src/process.js +2 -2
  100. package/packages/daemon/src/providers/local.js +36 -8
  101. package/packages/daemon/src/registry.js +21 -5
  102. package/packages/daemon/src/routes/agents.js +889 -0
  103. package/packages/daemon/src/routes/coordination.js +318 -0
  104. package/packages/daemon/src/routes/files.js +751 -0
  105. package/packages/daemon/src/routes/integrations.js +485 -0
  106. package/packages/daemon/src/routes/network.js +1784 -0
  107. package/packages/daemon/src/routes/providers.js +755 -0
  108. package/packages/daemon/src/routes/schedules.js +110 -0
  109. package/packages/daemon/src/routes/teams.js +650 -0
  110. package/packages/daemon/src/scheduler.js +456 -24
  111. package/packages/daemon/src/teams.js +1 -1
  112. package/packages/daemon/src/validate.js +38 -1
  113. package/packages/daemon/templates/mlx-setup.json +12 -0
  114. package/packages/daemon/templates/tgi-setup.json +1 -1
  115. package/packages/daemon/templates/vllm-setup.json +1 -1
  116. package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  117. package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  118. package/packages/gui/dist/index.html +2 -2
  119. package/packages/gui/package.json +1 -1
  120. package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
  121. package/packages/gui/src/app.css +35 -0
  122. package/packages/gui/src/components/agents/agent-config.jsx +1 -128
  123. package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
  124. package/packages/gui/src/components/agents/agent-node.jsx +8 -13
  125. package/packages/gui/src/components/agents/code-review.jsx +159 -122
  126. package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
  127. package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
  128. package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
  129. package/packages/gui/src/components/automations/automation-card.jsx +274 -0
  130. package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
  131. package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
  132. package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
  133. package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
  134. package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  135. package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
  136. package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
  137. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  138. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  139. package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
  140. package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
  141. package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
  142. package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
  143. package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
  144. package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
  145. package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
  146. package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
  147. package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
  148. package/packages/gui/src/components/network/network-health.jsx +2 -2
  149. package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
  150. package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
  151. package/packages/gui/src/components/ui/sheet.jsx +5 -2
  152. package/packages/gui/src/lib/cron.js +64 -0
  153. package/packages/gui/src/lib/status.js +24 -24
  154. package/packages/gui/src/lib/theme-hex.js +1 -0
  155. package/packages/gui/src/stores/groove.js +34 -3144
  156. package/packages/gui/src/stores/helpers.js +10 -0
  157. package/packages/gui/src/stores/slices/agents-slice.js +452 -0
  158. package/packages/gui/src/stores/slices/automations-slice.js +96 -0
  159. package/packages/gui/src/stores/slices/chat-slice.js +227 -0
  160. package/packages/gui/src/stores/slices/editor-slice.js +285 -0
  161. package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
  162. package/packages/gui/src/stores/slices/network-slice.js +361 -0
  163. package/packages/gui/src/stores/slices/preview-slice.js +109 -0
  164. package/packages/gui/src/stores/slices/providers-slice.js +897 -0
  165. package/packages/gui/src/stores/slices/teams-slice.js +413 -0
  166. package/packages/gui/src/stores/slices/ui-slice.js +98 -0
  167. package/packages/gui/src/views/agents.jsx +5 -5
  168. package/packages/gui/src/views/dashboard.jsx +12 -13
  169. package/packages/gui/src/views/marketplace.jsx +191 -3
  170. package/packages/gui/src/views/model-lab.jsx +17 -6
  171. package/packages/gui/src/views/models.jsx +410 -509
  172. package/packages/gui/src/views/network.jsx +3 -3
  173. package/packages/gui/src/views/settings.jsx +81 -94
  174. package/packages/gui/src/views/teams.jsx +40 -483
  175. package/SECURITY_SWEEP.md +0 -228
  176. package/TRAINING_DATA_v4.md +0 -6
  177. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
  178. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
  179. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
  180. package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
  181. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
  182. package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
  183. package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
  184. package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
  185. package/packages/gui/src/views/preview.jsx +0 -6
  186. package/packages/gui/src/views/subscription-panel.jsx +0 -327
  187. package/test.py +0 -571
package/test.py DELETED
@@ -1,571 +0,0 @@
1
- #!/usr/bin/python3
2
- import os
3
- from dotenv import load_dotenv
4
- import mysql.connector
5
- import logging
6
- import csv
7
- import urllib.parse
8
-
9
- import json
10
- from openai import OpenAI
11
- import time
12
-
13
- # Load environment variables
14
- env_path = "/var/www/html/security/.env"
15
- load_dotenv(dotenv_path=env_path)
16
-
17
- # Logging setup
18
- logging.basicConfig(
19
- filename="/var/www/facebook/catalog/logs/pull-inventory-facebook.log",
20
- level=logging.INFO,
21
- format="%(asctime)s %(levelname)s: %(message)s"
22
- )
23
-
24
- # Database credentials
25
- DB_PASS = os.getenv('PASSWORD')
26
- DATABASE = os.getenv('DBNAME_CLIENT')
27
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
28
-
29
- def get_facebook_body_style(body_field_from_vauto):
30
- """
31
- Translates a descriptive body field (e.g., "4D Sport Utility")
32
- into a Facebook-approved body style enum.
33
- """
34
- if not body_field_from_vauto:
35
- return 'OTHER'
36
- body = body_field_from_vauto.lower()
37
- if 'convertible' in body:
38
- return 'CONVERTIBLE'
39
- elif 'crewmax' in body or 'crew cab' in body or 'supercrew' in body or 'quad cab' in body or 'pickup' in body or 'truck' in body or 'xtra cab' in body:
40
- return 'TRUCK'
41
- elif 'sport utility' in body or 'suv' in body:
42
- return 'SUV'
43
- elif 'coupe' in body:
44
- return 'COUPE'
45
- elif 'passenger van' in body or 'minivan' in body or 'van' in body:
46
- return 'VAN'
47
- elif 'sedan' in body:
48
- return 'SEDAN'
49
- elif 'hatchback' in body:
50
- return 'HATCHBACK'
51
- elif 'wagon' in body:
52
- return 'WAGON'
53
- else:
54
- return 'OTHER'
55
-
56
- def connect_to_db():
57
- """Connect to MySQL database."""
58
- try:
59
- conn = mysql.connector.connect(
60
- host="localhost",
61
- user="root",
62
- password=DB_PASS,
63
- database=DATABASE
64
- )
65
- return conn
66
- except mysql.connector.Error as e:
67
- logging.error(f"Database connection error: {e}")
68
- return None
69
-
70
- # --- RENAMED: This function gets dealer *config*, not vAuto data ---
71
- def get_facebook_dealers():
72
- """Fetch dealers with Facebook integration."""
73
- conn = connect_to_db()
74
- if not conn:
75
- return []
76
-
77
- cursor = conn.cursor(dictionary=True)
78
- try:
79
- # This query is correct, it's fetching dealers *to process*
80
- cursor.execute("SELECT id, dealer_name, search_link_new, search_link_used, address, city, state, postal_code, phone, privacy_policy_url, inventory_type, campaign_product_id, ad_prospecting_id, page_id FROM facebook_dealers WHERE process_catalog = true AND dealer_name = 'San Jose Mitsubishi'")
81
- dealers = cursor.fetchall()
82
- logging.info(f"Retrieved {len(dealers)} dealers for Facebook feed generation.")
83
- return dealers
84
- except mysql.connector.Error as e:
85
- logging.error(f"Error fetching dealers: {e}")
86
- return []
87
- finally:
88
- cursor.close()
89
- conn.close()
90
-
91
- # --- NEW: Function to get inventory from dealer_inventory table ---
92
- def get_dealer_inventory(dealer_name):
93
- """Fetch all active inventory for a specific dealer from the database."""
94
- conn = connect_to_db()
95
- if not conn:
96
- return []
97
-
98
- cursor = conn.cursor(dictionary=True) # dictionary=True is key!
99
- try:
100
- cursor.execute("""
101
- SELECT * FROM dealer_inventory
102
- WHERE dealer_name = %s AND vehicle_status = 'active'
103
- """, (dealer_name,))
104
- inventory = cursor.fetchall()
105
- logging.info(f"Retrieved {len(inventory)} active vehicles from DB for {dealer_name}")
106
- return inventory
107
- except mysql.connector.Error as e:
108
- logging.error(f"Error fetching inventory for {dealer_name}: {e}")
109
- return []
110
- finally:
111
- cursor.close()
112
- conn.close()
113
-
114
-
115
- def get_vehicle_copy(dealer_name, vin):
116
- conn = connect_to_db()
117
- if not conn:
118
- return None
119
- try:
120
- cursor = conn.cursor(dictionary=True)
121
- cursor.execute("""
122
- SELECT * FROM facebook_vehicle_copy
123
- WHERE dealer_name = %s AND vin = %s
124
- """, (dealer_name, vin))
125
- result = cursor.fetchone()
126
- return result
127
- finally:
128
- cursor.close()
129
- conn.close()
130
-
131
-
132
- def save_vehicle_copy(dealer_name, vin, copy_data):
133
- conn = connect_to_db()
134
- if not conn:
135
- return False
136
- try:
137
- cursor = conn.cursor()
138
- query = """
139
- INSERT INTO facebook_vehicle_copy
140
- (dealer_name, vin, custom_headline, custom_description,
141
- custom_headline_retarget, custom_description_retarget, raw_json)
142
- VALUES (%s, %s, %s, %s, %s, %s, %s)
143
- ON DUPLICATE KEY UPDATE
144
- custom_headline = VALUES(custom_headline),
145
- custom_description = VALUES(custom_description),
146
- custom_headline_retarget = VALUES(custom_headline_retarget),
147
- custom_description_retarget = VALUES(custom_description_retarget),
148
- raw_json = VALUES(raw_json)
149
- """
150
- cursor.execute(query, (
151
- dealer_name, vin,
152
- copy_data.get("custom_headline"),
153
- copy_data.get("custom_description"),
154
- copy_data.get("custom_headline_retarget"),
155
- copy_data.get("custom_description_retarget"),
156
- json.dumps(copy_data)
157
- ))
158
- conn.commit()
159
- return True
160
- finally:
161
- cursor.close()
162
- conn.close()
163
-
164
-
165
- # --- MODIFIED: Handles DB data types (int, Decimal) and column names ---
166
- def create_prospecting_copy(dealer_name, vehicle):
167
- """Generate AI ad copy from a DB inventory row (dictionary)."""
168
- # Normalize key names from DB and cast non-strings
169
- year = str(vehicle.get('Year', ''))
170
- make = (vehicle.get('Make', '') or '').strip()
171
- model = (vehicle.get('Model', '') or '').strip()
172
- series = (vehicle.get('Series', '') or '').strip()
173
- engine = (vehicle.get('Engine', '') or '').strip()
174
- days_on_lot = str(vehicle.get('Age', ''))
175
- vin = (vehicle.get('VIN', '') or '').strip()
176
- price = str(vehicle.get('Price', ''))
177
- body = (vehicle.get('Body', '') or '').strip()
178
- transmission = (vehicle.get('Transmission', '') or '').strip()
179
- drivetrain = (vehicle.get('Drivetrain_Desc', '') or '').strip() # DB col: Drivetrain_Desc
180
- mileage = str(vehicle.get('Odometer', ''))
181
- fuel = (vehicle.get('Fuel', '') or '').strip()
182
- colour = (vehicle.get('Colour', '') or '').strip()
183
- features = (vehicle.get('Features', '') or '').strip()
184
- description = (vehicle.get('Description', '') or '').strip()
185
-
186
- # Handle the "New_Used" column from DB
187
- vehicle_new_used = (vehicle.get('New_Used') or '').strip().upper() # DB col: New_Used
188
- if vehicle_new_used == 'U':
189
- vehicle_new_used = 'a Used vehicle'
190
- elif vehicle_new_used == 'N':
191
- vehicle_new_used = 'a Brand New vehicle'
192
- else:
193
- logging.warning(f"VIN {vin}: Missing or invalid 'New_Used' value — defaulting to Not Sure")
194
- vehicle_new_used = 'Not Sure'
195
-
196
- # Log what we’re passing to the AI
197
- logging.info(f"{dealer_name}: Processing {year} {make} {model} ({vin}) as {vehicle_new_used}")
198
-
199
- # Construct the AI prompt
200
- prompt = f"""
201
- You are an expert automotive copywriter and Facebook Ads strategist.
202
- Your task is to take raw vehicle data and transform it into a complete, high-performing ad creative package,
203
- formatted as a single JSON object. Your tone should be professional, yet conversational and personal.
204
- Your goal is to generate excitement and trust, encouraging potential buyers to visit the dealership.
205
-
206
- **CRITICAL RULES & CONSTRAINTS:**
207
- - FINAL OUTPUT MUST BE A SINGLE, VALID JSON OBJECT AND NOTHING ELSE.
208
- - ABSOLUTELY NO EMOJIS or HASHTAGS.
209
- - Adhere strictly to character limits.
210
- - Focus on the vehicle’s features, condition, and dealership value.
211
- - Do not discuss credit, payments, or financing (Facebook will flag it).
212
- - Include price naturally within the first two sentences.
213
- - Avoid claims like “lowest price” or unrealistic offers.
214
- - If no price or price is 0, do not mention it anywhere in the ad.
215
-
216
- **VEHICLE DATA FOR THIS AD:**
217
- Dealer Name: {dealer_name}
218
- - {year} {make} {model} {series}
219
- - Body Style: {body}
220
- - Transmission: {transmission}
221
- - Drivetrain: {drivetrain}
222
- - Mileage: {mileage}
223
- - Fuel Type: {fuel}
224
- - Color: {colour}
225
- - Price: ${price}
226
- - Key Features: {features}
227
- - Dealer Description: {description}
228
- - Vehicle Type: {vehicle_new_used}
229
- - Days on Lot: {days_on_lot}
230
-
231
- If it's a new vehicle, do not mention mileage.
232
- If it's used, mention low mileage only when relevant.
233
-
234
- **TASK: Generate the following components as valid JSON**
235
- 1. `primary_text`: Full ad copy (first sentence under 125 characters, persuasive CTA to visit {dealer_name})
236
- 2. `headlines`: 3 distinct headlines (each under 40 characters)
237
- 3. `link_descriptions`: 2 short value-driven descriptions (each under 30 characters)
238
- 4. `call_to_action_type`: One from [SHOP_NOW, GET_QUOTE]
239
-
240
- **FINAL JSON OUTPUT FORMAT:**
241
- {{
242
- "primary_text": "...",
243
- "headlines": ["...", "...", "..."],
244
- "link_descriptions": ["...", "..."],
245
- "call_to_action_type": "..."
246
- }}
247
- """
248
-
249
- max_retries = 3
250
- for attempt in range(max_retries):
251
- try:
252
- response = client.chat.completions.create(
253
- model="gpt-5-mini", # ✅ Kept your specified model
254
- messages=[
255
- {"role": "system", "content": "You are a Facebook Ads expert. Respond in clean JSON only."},
256
- {"role": "user", "content": prompt}
257
- ],
258
- response_format={"type": "json_object"},
259
- reasoning_effort="low", # ✅ Kept your original parameters
260
- store=False
261
- )
262
- ai_response = response.choices[0].message.content.strip()
263
-
264
- try:
265
- json.loads(ai_response)
266
- return ai_response # On success, return the response
267
- except json.JSONDecodeError:
268
- logging.warning(f"AI for {vin} returned invalid JSON on attempt {attempt + 1}. Retrying...")
269
- # Let the loop continue to the next attempt
270
-
271
- except Exception as e:
272
- logging.warning(f"AI generation for VIN {vin} failed on attempt {attempt + 1}/{max_retries}. Error: {e}")
273
-
274
- if attempt < max_retries - 1: # Don't sleep on the final failed attempt
275
- time.sleep(2) # Wait 2 seconds before retrying
276
-
277
- logging.error(f"AI generation for {vin} failed after all retries.")
278
- return None # If all retries fail, return None
279
-
280
-
281
- # --- RENAMED & MODIFIED: Reads from DB data, not CSV file ---
282
- def transform_db_inventory_for_facebook(dealer, output_csv, inv_type, campaign_id, prospecting_ad_id):
283
- """
284
- Convert DB inventory data to Facebook Vehicle Catalog format and
285
- write it to *output_csv*.
286
- """
287
- temp_output_csv = f"{output_csv}.tmp"
288
- MAX_IMAGES = 20 # Facebook's limit for images
289
-
290
- # --- NEW: Get inventory from database ---
291
- inventory_data = get_dealer_inventory(dealer['dealer_name'])
292
- if not inventory_data:
293
- logging.warning(f"No active inventory found in DB for {dealer['dealer_name']}. Skipping feed generation.")
294
- return
295
-
296
- try:
297
- # --- REMOVED: `with open(csv_file, 'r', ...)` ---
298
- with open(temp_output_csv, 'w', encoding='utf-8', newline='') as outfile:
299
-
300
- # Define base fieldnames and add dynamic image fields
301
- fieldnames = [
302
- "vehicle_id", "vehicle_registration_plate", "vin", "make", "model", "year",
303
- "transmission", "body_style", "fuel_type", "drivetrain", "description",
304
- ]
305
- # Add image fields: image[0].url, image[1].url, etc.
306
- fieldnames.extend([f"image[{i}].url" for i in range(MAX_IMAGES)])
307
- fieldnames.extend([
308
- "mileage.value", "mileage.unit", "url", "title", "price",
309
- "state_of_vehicle", "exterior_color", "address", "latitude", "longitude",
310
- "trim", "interior_colour", "dealer_id", "dealer_name", "postal_code",
311
- "dealer_phone", "fb_page_id", "dealer_communication_channel",
312
- "dealer_privacy_policy_url"
313
- ])
314
-
315
- writer = csv.DictWriter(outfile, fieldnames=fieldnames)
316
- writer.writeheader()
317
-
318
- def force_https(url: str) -> str:
319
- return url.replace("http://", "https://", 1) if url and url.startswith("http://") else url
320
-
321
- # for testing to limit vehicles
322
- # MAX_TEST_VEHICLES = 2 # limit for testing
323
- # count = 0
324
-
325
- # --- MODIFIED: Loop over DB inventory list, not CSV reader ---
326
- for row in inventory_data:
327
- # for testing to limit vehicles
328
- # if count >= MAX_TEST_VEHICLES:
329
- # break
330
- # count += 1
331
-
332
- # --- REMOVED: 'sold' check (already done in SQL) ---
333
-
334
- vin = (row.get("VIN") or "").strip()
335
- if not vin:
336
- continue
337
-
338
- # DB col: New_Used
339
- db_type = (row.get('New_Used') or '').strip().upper() # 'N' or 'U'
340
- if inv_type == 'new' and db_type != 'N':
341
- continue
342
- elif inv_type == 'used' and db_type != 'U':
343
- continue
344
-
345
- cached_copy = get_vehicle_copy(dealer['dealer_name'], vin)
346
- if cached_copy and cached_copy.get('custom_headline'):
347
- custom_headline = cached_copy['custom_headline']
348
- custom_description = cached_copy['custom_description']
349
- # Add other cached fields if needed
350
- else:
351
- # Generate new copy
352
- logging.info(f"Generating new AI copy for VIN: {vin}")
353
- ai_response = create_prospecting_copy(dealer['dealer_name'], row)
354
-
355
- custom_headline = ""
356
- custom_description = ""
357
-
358
- if ai_response: # Check if the AI returned anything at all
359
- try:
360
- copy_json = json.loads(ai_response)
361
- # Use .get() for safe access to prevent KeyErrors
362
- headlines = copy_json.get('headlines', [])
363
- primary_text = copy_json.get('primary_text', '')
364
-
365
- if headlines and primary_text:
366
- custom_headline = headlines[0]
367
- custom_description = primary_text
368
-
369
- # Only save if the generation was successful
370
- save_vehicle_copy(dealer['dealer_name'], vin, {
371
- "custom_headline": custom_headline,
372
- "custom_description": custom_description,
373
- "custom_headline_retarget": headlines[1] if len(headlines) > 1 else '',
374
- "custom_description_retarget": copy_json.get('link_descriptions', [''])[0],
375
- "raw": copy_json
376
- })
377
- else:
378
- logging.warning(f"AI response for {vin} was valid JSON but missing required keys (headlines/primary_text).")
379
-
380
- except json.JSONDecodeError:
381
- logging.error(f"Failed to decode AI JSON response for {vin}.")
382
- else:
383
- logging.error(f"AI generation failed for {vin}; no response was returned.")
384
-
385
-
386
- # Price from DB is Decimal, float() handles it
387
- price_val = float(row.get("Price") or 0)
388
- price = f"{price_val:.2f} USD" # Format: 19995.00 USD
389
-
390
- # Mileage from DB is int, str() handles it
391
- mileage_val = str(row.get("Odometer") or '0')
392
- mileage_unit = "MI" # Or "KM"
393
-
394
- year = str(row.get('Year', '')) # Year from DB is int
395
- make = row.get('Make', '')
396
- model = row.get('Model', '')
397
- # DB cols: Series_Detail, Series
398
- trim = (row.get('Series_Detail') or row.get('Series') or '').strip()
399
-
400
- vehicle_type = row.get('New_Used') # DB col: New_Used
401
-
402
- if vehicle_type == 'U':
403
- vehicle_type = 'used'
404
- else:
405
- vehicle_type = 'new'
406
-
407
- # Use pre-built VDP URL from inventory, fallback to search_link
408
- redirect_url = row.get('vdp_url')
409
- if not redirect_url:
410
- base_link = dealer['search_link_used'] if vehicle_type == 'used' else dealer['search_link_new']
411
- redirect_url = base_link.replace('{VIN}', str(vin))
412
-
413
- # Direct link to dealer VDP with tracking params (no redirect)
414
- _sep = "&" if "?" in redirect_url else "?"
415
- vehicle_url = redirect_url + _sep + urllib.parse.urlencode({
416
- "utm_source": "facebook",
417
- "utm_medium": "paid_social",
418
- "utm_campaign": "{{campaign.name}}",
419
- "utm_content": "{{adset.name}}",
420
- "utm_term": "{{ad.name}}",
421
- "ad_id": "{{ad.id}}",
422
- "adset_id": "{{adset.id}}",
423
- "campaign_id": "{{campaign.id}}",
424
- "site_source": "{{site_source_name}}",
425
- "placement": "{{placement}}",
426
- })
427
-
428
- # Title
429
- title = f"{year} {make} {model} {trim}".strip()
430
-
431
- # Address JSON object
432
- address_obj = {
433
- "addr1": dealer.get('address', 'N/A'),
434
- "city": dealer.get('city', 'N/A'),
435
- "region": dealer.get('state', 'N/A'),
436
- "postal_code": dealer.get('postal_code', 'N/A'),
437
- "country": "US" # Assuming United States
438
- }
439
-
440
- # Transmission (Must be an approved enum)
441
- transmission_raw = (row.get('Transmission') or '').lower()
442
- if 'automatic' in transmission_raw or 'cvt' in transmission_raw:
443
- transmission = 'AUTOMATIC'
444
- elif 'manual' in transmission_raw:
445
- transmission = 'MANUAL'
446
- else:
447
- transmission = 'OTHER'
448
-
449
- # Drivetrain (Must be an approved enum)
450
- # DB col: Drivetrain_Desc
451
- drivetrain_raw = (row.get('Drivetrain_Desc') or '').upper()
452
- if drivetrain_raw in ['AWD', '4WD', '4X4']:
453
- drivetrain = 'ALL_WHEEL_DRIVE'
454
- elif drivetrain_raw == 'FWD':
455
- drivetrain = 'FRONT_WHEEL_DRIVE'
456
- elif drivetrain_raw == 'RWD':
457
- drivetrain = 'REAR_WHEEL_DRIVE'
458
- else:
459
- drivetrain = 'OTHER'
460
-
461
- # Fuel Type (Must be an approved enum)
462
- fuel_type_raw = (row.get('Fuel') or '').upper()
463
- if fuel_type_raw == 'GASOLINE':
464
- fuel_type = 'GASOLINE'
465
- elif fuel_type_raw == 'DIESEL':
466
- fuel_type = 'DIESEL'
467
- elif fuel_type_raw == 'ELECTRIC':
468
- fuel_type = 'ELECTRIC'
469
- elif fuel_type_raw in ('HYBRID', 'GASOLINE/HYBRID'):
470
- fuel_type = 'HYBRID_ENGINE'
471
- elif fuel_type_raw == 'FLEX FUEL':
472
- fuel_type = 'FLEX'
473
- else:
474
- fuel_type = 'OTHER'
475
-
476
- # --- IMAGE HANDLING ---
477
- # DB col: Photo_Url_List
478
- photos_raw = (row.get('Photo_Url_List') or '').split('|')
479
- images = [force_https(p.strip()) for p in photos_raw if p and p.strip()]
480
-
481
- # Skip if fewer than 4 usable images
482
- if len(images) < 5:
483
- continue
484
-
485
- # Write up to MAX_IMAGES valid URLs
486
- image_dict = {}
487
- for i, img_url in enumerate(images[:MAX_IMAGES]):
488
- image_dict[f"image[{i}].url"] = img_url
489
-
490
-
491
- # --- ROW ASSEMBLY ---
492
- output_row = {
493
- 'vehicle_id': vin,
494
- 'vin': vin,
495
- 'make': make,
496
- 'model': model,
497
- 'year': year,
498
- 'transmission': transmission,
499
- 'body_style': get_facebook_body_style(row.get('Body')),
500
- 'fuel_type': fuel_type,
501
- 'drivetrain': drivetrain,
502
- 'description': custom_description or f"Explore this {year} {make} {model} at {dealer['dealer_name']} today!",
503
- 'mileage.value': mileage_val,
504
- 'mileage.unit': mileage_unit,
505
- 'url': vehicle_url,
506
- 'title': custom_headline or title,
507
- 'price': price,
508
- 'state_of_vehicle': vehicle_type,
509
- 'exterior_color': row.get('Colour', ''),
510
- 'address': json.dumps(address_obj),
511
- 'trim': trim,
512
- 'interior_colour': row.get('Interior_Color', ''), # DB col: Interior_Color
513
- 'dealer_id': dealer.get('id'), # Your internal dealer ID
514
- 'dealer_name': dealer.get('dealer_name'),
515
- 'postal_code': dealer.get('postal_code', ''),
516
- 'dealer_phone': dealer.get('phone', ''),
517
- 'fb_page_id': dealer.get('fb_page_id', ''),
518
- 'dealer_communication_channel': 'CHAT', # Or 'PHONE_CALL', 'LEAD_FORM'
519
- 'dealer_privacy_policy_url': dealer.get('privacy_policy_url', '')
520
- }
521
-
522
- # Add the dynamic image fields to the row
523
- output_row.update(image_dict)
524
-
525
- writer.writerow(output_row)
526
-
527
- os.replace(temp_output_csv, output_csv)
528
- logging.info(f"Transformed Facebook CSV for {dealer['dealer_name']} written to {output_csv}")
529
-
530
- except Exception as e:
531
- logging.error(f"Error transforming Facebook CSV for {dealer['dealer_name']}: {e}", exc_info=True)
532
- if os.path.exists(temp_output_csv):
533
- os.remove(temp_output_csv)
534
-
535
-
536
-
537
- def format_filename_component(value):
538
- """Formats a string for use in a filename."""
539
- if value:
540
- return value.replace(' ', '_').replace('&', 'and')
541
- return ''
542
-
543
- # --- MODIFIED: Main function to call the new DB-driven functions ---
544
- def process_inventory():
545
- """Main function to process inventory from DB and generate Facebook Catalog CSV."""
546
- dealers = get_facebook_dealers() # Renamed function
547
- if not dealers:
548
- logging.info("No dealers found with Facebook integration enabled.")
549
- return
550
-
551
- for dealer in dealers:
552
- dealer_name = dealer["dealer_name"]
553
- dealer_id = dealer["id"]
554
- inv_type = dealer['inventory_type']
555
-
556
- campaign_id = dealer['campaign_product_id']
557
- prospecting_ad_id = dealer['campaign_product_id']
558
-
559
- # --- REMOVED: csv_file path and os.path.exists check ---
560
- safe_dealer_name = format_filename_component(dealer_name)
561
- # --- MODIFIED: Changed "vauto_feed" to "db_feed" for clarity ---
562
- output_csv = f"/var/www/html/app.studio19.io/fb_catalog_csv/{safe_dealer_name}_db_feed_english.csv"
563
-
564
- logging.info(f"Processing DB inventory for {dealer_name}")
565
-
566
- # --- MODIFIED: Call renamed function and pass correct args ---
567
- transform_db_inventory_for_facebook(dealer, output_csv, inv_type, campaign_id, prospecting_ad_id)
568
-
569
-
570
- if __name__ == "__main__":
571
- process_inventory()