@land-catalyst/batch-data-sdk 1.5.1 → 1.5.3
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/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./property-field/display";
|
|
|
16
16
|
export * from "./property-field/search-criteria-filter-context";
|
|
17
17
|
export type { SearchCriteriaFilterContext, SearchCriteriaFilterType, } from "./property-field/search-criteria-filter-context";
|
|
18
18
|
export * from "./property-field/search-criteria-ai-context";
|
|
19
|
+
export * from "./property-field/property-data-ai-context";
|
|
19
20
|
export { type PropertyFieldPathType, type PropertyFieldValueType, type FieldMetadataForPath, } from "./property-field/types";
|
|
20
21
|
export type { SearchCriteriaFieldMapping, SearchCriteriaFieldMetadata, } from "./property-field/utils";
|
|
21
22
|
export type { RequestMiddleware, ResponseMiddleware, ErrorMiddleware, HttpMiddleware, } from "./client/client";
|
package/dist/index.js
CHANGED
|
@@ -31,3 +31,4 @@ __exportStar(require("./property-field/utils"), exports);
|
|
|
31
31
|
__exportStar(require("./property-field/display"), exports);
|
|
32
32
|
__exportStar(require("./property-field/search-criteria-filter-context"), exports);
|
|
33
33
|
__exportStar(require("./property-field/search-criteria-ai-context"), exports);
|
|
34
|
+
__exportStar(require("./property-field/property-data-ai-context"), exports);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property Data AI Context Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds context for AI services to analyze and discuss property data.
|
|
5
|
+
* Provides field descriptions, interpretations, and investment insights.
|
|
6
|
+
*
|
|
7
|
+
* This module focuses on Property RESPONSE fields (data returned from searches),
|
|
8
|
+
* not SearchCriteria filter fields. Use search-criteria-ai-context.ts for search filters.
|
|
9
|
+
*/
|
|
10
|
+
import type { Property } from "../core/types";
|
|
11
|
+
/**
|
|
12
|
+
* Property data domain with description and investment relevance
|
|
13
|
+
*/
|
|
14
|
+
export interface PropertyDataDomain {
|
|
15
|
+
/** Domain name (e.g., "valuation", "permit", "owner") */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Human-readable description */
|
|
18
|
+
description: string;
|
|
19
|
+
/** Why this domain matters for investment analysis */
|
|
20
|
+
investmentRelevance: string;
|
|
21
|
+
/** Key fields to highlight in this domain */
|
|
22
|
+
keyFields?: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* All property data domains with descriptions and investment relevance
|
|
26
|
+
*/
|
|
27
|
+
export declare const PROPERTY_DATA_DOMAINS: PropertyDataDomain[];
|
|
28
|
+
/**
|
|
29
|
+
* Options for building property data AI context
|
|
30
|
+
*/
|
|
31
|
+
export interface PropertyDataAIContextOptions {
|
|
32
|
+
/** Domains to include (defaults to all) */
|
|
33
|
+
domains?: string[];
|
|
34
|
+
/** Whether to include raw field values */
|
|
35
|
+
includeRawValues?: boolean;
|
|
36
|
+
/** Whether to include field metadata descriptions */
|
|
37
|
+
includeFieldDescriptions?: boolean;
|
|
38
|
+
/** Maximum fields per domain to include */
|
|
39
|
+
maxFieldsPerDomain?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extracted property data with AI-friendly formatting
|
|
43
|
+
*/
|
|
44
|
+
export interface PropertyDataContext {
|
|
45
|
+
/** The property address formatted for display */
|
|
46
|
+
address: string;
|
|
47
|
+
/** Key property metrics for quick reference */
|
|
48
|
+
summary: {
|
|
49
|
+
estimatedValue?: number;
|
|
50
|
+
equity?: number;
|
|
51
|
+
equityPercent?: number;
|
|
52
|
+
ltv?: number;
|
|
53
|
+
salePropensity?: number;
|
|
54
|
+
salePropensityCategory?: string;
|
|
55
|
+
yearBuilt?: number;
|
|
56
|
+
sqft?: number;
|
|
57
|
+
bedrooms?: number;
|
|
58
|
+
bathrooms?: number;
|
|
59
|
+
propertyType?: string;
|
|
60
|
+
ownerName?: string;
|
|
61
|
+
ownerType?: string;
|
|
62
|
+
permitCount?: number;
|
|
63
|
+
ownerPortfolioSize?: number;
|
|
64
|
+
};
|
|
65
|
+
/** Motivation signals detected */
|
|
66
|
+
motivationSignals: string[];
|
|
67
|
+
/** Investment considerations */
|
|
68
|
+
considerations: string[];
|
|
69
|
+
/** Domain-specific data */
|
|
70
|
+
domains: Record<string, Record<string, unknown>>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build comprehensive property data context for AI
|
|
74
|
+
* @param property The property object to analyze
|
|
75
|
+
* @param options Configuration options
|
|
76
|
+
* @returns Structured context for AI consumption
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const context = buildPropertyDataContext(property);
|
|
80
|
+
* // Use context.summary for quick metrics
|
|
81
|
+
* // Use context.motivationSignals for seller motivation
|
|
82
|
+
* // Use context.considerations for investment analysis
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export declare function buildPropertyDataContext(property: Property, options?: PropertyDataAIContextOptions): PropertyDataContext;
|
|
86
|
+
/**
|
|
87
|
+
* Build AI system prompt section for property data context
|
|
88
|
+
* @param context The property data context
|
|
89
|
+
* @returns Formatted string for AI system prompt
|
|
90
|
+
*/
|
|
91
|
+
export declare function formatPropertyContextForAI(context: PropertyDataContext): string;
|
|
92
|
+
/**
|
|
93
|
+
* Get domain descriptions for AI context
|
|
94
|
+
* @param domainNames Optional list of domains to include
|
|
95
|
+
* @returns Formatted domain descriptions
|
|
96
|
+
*/
|
|
97
|
+
export declare function getPropertyDomainDescriptions(domainNames?: string[]): string;
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Property Data AI Context Builder
|
|
4
|
+
*
|
|
5
|
+
* Builds context for AI services to analyze and discuss property data.
|
|
6
|
+
* Provides field descriptions, interpretations, and investment insights.
|
|
7
|
+
*
|
|
8
|
+
* This module focuses on Property RESPONSE fields (data returned from searches),
|
|
9
|
+
* not SearchCriteria filter fields. Use search-criteria-ai-context.ts for search filters.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PROPERTY_DATA_DOMAINS = void 0;
|
|
13
|
+
exports.buildPropertyDataContext = buildPropertyDataContext;
|
|
14
|
+
exports.formatPropertyContextForAI = formatPropertyContextForAI;
|
|
15
|
+
exports.getPropertyDomainDescriptions = getPropertyDomainDescriptions;
|
|
16
|
+
const utils_1 = require("./utils");
|
|
17
|
+
/**
|
|
18
|
+
* All property data domains with descriptions and investment relevance
|
|
19
|
+
*/
|
|
20
|
+
exports.PROPERTY_DATA_DOMAINS = [
|
|
21
|
+
{
|
|
22
|
+
name: "address",
|
|
23
|
+
description: "Property location and address details",
|
|
24
|
+
investmentRelevance: "Location drives value appreciation, rental rates, and exit strategy options. Neighborhood, school districts, and proximity to amenities affect marketability.",
|
|
25
|
+
keyFields: ["street", "city", "state", "zip", "county"],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "building",
|
|
29
|
+
description: "Building characteristics, size, and features",
|
|
30
|
+
investmentRelevance: "Building specs determine renovation costs, rental potential, and buyer appeal. Year built affects maintenance costs; square footage and bedroom/bath count drive value.",
|
|
31
|
+
keyFields: [
|
|
32
|
+
"yearBuilt",
|
|
33
|
+
"size",
|
|
34
|
+
"bedroomCount",
|
|
35
|
+
"bathCount",
|
|
36
|
+
"generalPropertyType",
|
|
37
|
+
"buildingCondition",
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "valuation",
|
|
42
|
+
description: "Property value estimates including AVM, equity, and loan-to-value",
|
|
43
|
+
investmentRelevance: "AVM provides baseline value for offer calculations. Equity indicates owner flexibility; high equity owners may accept creative deals. LTV over 80% suggests potential distress.",
|
|
44
|
+
keyFields: [
|
|
45
|
+
"estimatedValue",
|
|
46
|
+
"equityCurrentEstimatedBalance",
|
|
47
|
+
"equityPercent",
|
|
48
|
+
"ltv",
|
|
49
|
+
"confidenceScore",
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "permit",
|
|
54
|
+
description: "Building permit history and renovation activity",
|
|
55
|
+
investmentRelevance: "Recent permits indicate property condition and improvements. High permit activity may signal deferred maintenance being addressed or active renovation. Solar/HVAC permits suggest energy efficiency upgrades.",
|
|
56
|
+
keyFields: [
|
|
57
|
+
"permitCount",
|
|
58
|
+
"totalJobValue",
|
|
59
|
+
"latestDate",
|
|
60
|
+
"allTags",
|
|
61
|
+
"tags",
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "propertyOwnerProfile",
|
|
66
|
+
description: "Owner portfolio size and financial position across all owned properties",
|
|
67
|
+
investmentRelevance: "Portfolio owners may be more sophisticated sellers but also more motivated if over-leveraged. High mortgage balance across portfolio suggests potential cash flow pressure. Multiple properties indicate experienced investor who may want quick, clean deals.",
|
|
68
|
+
keyFields: [
|
|
69
|
+
"propertiesCount",
|
|
70
|
+
"propertiesTotalEquity",
|
|
71
|
+
"propertiesTotalEstimatedValue",
|
|
72
|
+
"mortgagesTotalBalance",
|
|
73
|
+
"mortgagesCount",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "owner",
|
|
78
|
+
description: "Property owner information and contact details",
|
|
79
|
+
investmentRelevance: "Owner type (individual vs corporate vs trust) affects negotiation approach. Absentee owners may be more motivated. Length of ownership indicates attachment level.",
|
|
80
|
+
keyFields: ["fullName", "type", "mailingAddress", "phoneNumbers", "emails"],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "intel",
|
|
84
|
+
description: "Predictive analytics including sale propensity",
|
|
85
|
+
investmentRelevance: "Sale propensity score (0-100) predicts likelihood to sell. Scores above 70 indicate high motivation. Combined with other signals (length of residence, life events), helps prioritize outreach.",
|
|
86
|
+
keyFields: [
|
|
87
|
+
"salePropensity",
|
|
88
|
+
"salePropensityCategory",
|
|
89
|
+
"lastSoldDate",
|
|
90
|
+
"lastSoldPrice",
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "openLien",
|
|
95
|
+
description: "Current mortgages and liens on the property",
|
|
96
|
+
investmentRelevance: "Mortgage balance vs value determines equity and seller flexibility. Multiple liens may indicate financial stress. High interest rates on existing mortgages may motivate seller.",
|
|
97
|
+
keyFields: [
|
|
98
|
+
"totalOpenLienCount",
|
|
99
|
+
"totalOpenLienBalance",
|
|
100
|
+
"mortgages",
|
|
101
|
+
"firstLoanAmount",
|
|
102
|
+
"firstLoanInterestRate",
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "assessment",
|
|
107
|
+
description: "Tax assessment values and market value estimates",
|
|
108
|
+
investmentRelevance: "Assessed value vs market value gap indicates potential appreciation not yet taxed. Assessment increases may pressure cash-strapped owners. Useful for estimating property taxes.",
|
|
109
|
+
keyFields: [
|
|
110
|
+
"totalAssessedValue",
|
|
111
|
+
"totalMarketValue",
|
|
112
|
+
"assessmentYear",
|
|
113
|
+
"landValue",
|
|
114
|
+
"improvementValue",
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "quickLists",
|
|
119
|
+
description: "Pre-computed property flags and characteristics",
|
|
120
|
+
investmentRelevance: "Quick indicators of motivated seller situations. Vacant, pre-foreclosure, tax delinquent, and inherited properties often have motivated sellers. Free-and-clear properties have maximum flexibility.",
|
|
121
|
+
keyFields: [
|
|
122
|
+
"vacant",
|
|
123
|
+
"preforeclosure",
|
|
124
|
+
"taxDefault",
|
|
125
|
+
"inherited",
|
|
126
|
+
"freeAndClear",
|
|
127
|
+
"highEquity",
|
|
128
|
+
"absenteeOwner",
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "foreclosure",
|
|
133
|
+
description: "Foreclosure status and auction information",
|
|
134
|
+
investmentRelevance: "Active foreclosure indicates highly motivated seller with timeline pressure. Auction dates create urgency. Pre-foreclosure is often best window for negotiation.",
|
|
135
|
+
keyFields: ["status", "auctionDate", "defaultAmount", "filingDate"],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "sale",
|
|
139
|
+
description: "Sale history and transaction records",
|
|
140
|
+
investmentRelevance: "Recent sale price establishes baseline. Rapid appreciation or depreciation indicates market trends. Multiple sales in short period may indicate flipper activity or problem property.",
|
|
141
|
+
keyFields: [
|
|
142
|
+
"lastSaleDate",
|
|
143
|
+
"lastSalePrice",
|
|
144
|
+
"lastSaleType",
|
|
145
|
+
"priorSaleDate",
|
|
146
|
+
"priorSalePrice",
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "demographics",
|
|
151
|
+
description: "Owner demographic information",
|
|
152
|
+
investmentRelevance: "Demographics inform communication approach. Senior owners may be considering downsizing. Income and net worth estimates help gauge financial position and deal size capability.",
|
|
153
|
+
keyFields: ["estimatedAge", "estimatedIncome", "estimatedNetWorth"],
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
/**
|
|
157
|
+
* Extract key motivation signals from property data
|
|
158
|
+
*/
|
|
159
|
+
function extractMotivationSignals(property) {
|
|
160
|
+
const signals = [];
|
|
161
|
+
const ql = property.quickLists;
|
|
162
|
+
const intel = property.intel;
|
|
163
|
+
const valuation = property.valuation;
|
|
164
|
+
const ownerProfile = property
|
|
165
|
+
.propertyOwnerProfile;
|
|
166
|
+
// QuickList signals
|
|
167
|
+
if (ql?.vacant)
|
|
168
|
+
signals.push("Property appears vacant");
|
|
169
|
+
if (ql?.preforeclosure)
|
|
170
|
+
signals.push("Property is in pre-foreclosure");
|
|
171
|
+
if (ql?.taxDefault)
|
|
172
|
+
signals.push("Property taxes are delinquent");
|
|
173
|
+
if (ql?.inherited)
|
|
174
|
+
signals.push("Property was inherited");
|
|
175
|
+
if (ql?.absenteeOwner)
|
|
176
|
+
signals.push("Owner does not live at property");
|
|
177
|
+
if (ql?.outOfStateOwner)
|
|
178
|
+
signals.push("Owner lives out of state");
|
|
179
|
+
if (ql?.seniorOwner)
|
|
180
|
+
signals.push("Owner is a senior (65+)");
|
|
181
|
+
if (ql?.freeAndClear)
|
|
182
|
+
signals.push("Property is free and clear (no mortgage)");
|
|
183
|
+
if (ql?.highEquity)
|
|
184
|
+
signals.push("High equity position");
|
|
185
|
+
if (ql?.lowEquity)
|
|
186
|
+
signals.push("Low equity position");
|
|
187
|
+
// Sale propensity
|
|
188
|
+
const salePropensity = intel?.salePropensity;
|
|
189
|
+
if (salePropensity !== undefined) {
|
|
190
|
+
if (salePropensity >= 80) {
|
|
191
|
+
signals.push(`Very high sale propensity (${salePropensity}%)`);
|
|
192
|
+
}
|
|
193
|
+
else if (salePropensity >= 60) {
|
|
194
|
+
signals.push(`High sale propensity (${salePropensity}%)`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// LTV signals
|
|
198
|
+
const ltv = valuation?.ltv;
|
|
199
|
+
if (ltv !== undefined) {
|
|
200
|
+
if (ltv > 100) {
|
|
201
|
+
signals.push(`Underwater property (LTV: ${ltv.toFixed(0)}%)`);
|
|
202
|
+
}
|
|
203
|
+
else if (ltv > 80) {
|
|
204
|
+
signals.push(`High LTV may indicate financial pressure (${ltv.toFixed(0)}%)`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Portfolio stress signals
|
|
208
|
+
const portfolioMortgages = ownerProfile?.mortgagesTotalBalance;
|
|
209
|
+
const portfolioValue = ownerProfile?.propertiesTotalEstimatedValue;
|
|
210
|
+
if (portfolioMortgages && portfolioValue && portfolioMortgages > 0) {
|
|
211
|
+
const portfolioLtv = (portfolioMortgages / portfolioValue) * 100;
|
|
212
|
+
if (portfolioLtv > 70) {
|
|
213
|
+
signals.push(`Owner portfolio is leveraged (${portfolioLtv.toFixed(0)}% across all properties)`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return signals;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Generate investment considerations based on property data
|
|
220
|
+
*/
|
|
221
|
+
function generateConsiderations(property) {
|
|
222
|
+
const considerations = [];
|
|
223
|
+
const valuation = property.valuation;
|
|
224
|
+
const permit = property.permit;
|
|
225
|
+
const building = property.building;
|
|
226
|
+
// Equity-based considerations
|
|
227
|
+
const equity = valuation?.equityCurrentEstimatedBalance;
|
|
228
|
+
const estimatedValue = valuation?.estimatedValue;
|
|
229
|
+
if (equity && estimatedValue) {
|
|
230
|
+
const equityPercent = (equity / estimatedValue) * 100;
|
|
231
|
+
if (equityPercent > 50) {
|
|
232
|
+
considerations.push("High equity position gives seller flexibility on terms");
|
|
233
|
+
}
|
|
234
|
+
else if (equityPercent < 20) {
|
|
235
|
+
considerations.push("Low equity may limit seller's ability to negotiate on price");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Permit-based considerations
|
|
239
|
+
const permitCount = permit?.permitCount;
|
|
240
|
+
const permitTags = permit?.tags;
|
|
241
|
+
if (permitCount && permitCount > 0) {
|
|
242
|
+
considerations.push(`${permitCount} permits on file indicate renovation history`);
|
|
243
|
+
if (permitTags?.solar) {
|
|
244
|
+
considerations.push("Solar installation may reduce operating costs");
|
|
245
|
+
}
|
|
246
|
+
if (permitTags?.remodel || permitTags?.kitchen || permitTags?.bathroom) {
|
|
247
|
+
considerations.push("Recent remodel permits suggest updated interior");
|
|
248
|
+
}
|
|
249
|
+
if (permitTags?.roofing) {
|
|
250
|
+
considerations.push("Roofing permit suggests recent roof work");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Building condition considerations
|
|
254
|
+
const yearBuilt = building?.yearBuilt;
|
|
255
|
+
if (yearBuilt) {
|
|
256
|
+
const age = new Date().getFullYear() - yearBuilt;
|
|
257
|
+
if (age > 50) {
|
|
258
|
+
considerations.push(`Built ${age} years ago - may need significant updates`);
|
|
259
|
+
}
|
|
260
|
+
else if (age > 30) {
|
|
261
|
+
considerations.push(`Built ${age} years ago - likely needs some updates`);
|
|
262
|
+
}
|
|
263
|
+
else if (age < 10) {
|
|
264
|
+
considerations.push("Newer construction - likely lower maintenance costs");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Confidence considerations
|
|
268
|
+
const confidence = valuation?.confidenceScore;
|
|
269
|
+
if (confidence !== undefined) {
|
|
270
|
+
if (confidence < 50) {
|
|
271
|
+
considerations.push(`Low AVM confidence (${confidence}) - value estimate may be unreliable`);
|
|
272
|
+
}
|
|
273
|
+
else if (confidence >= 80) {
|
|
274
|
+
considerations.push(`High AVM confidence (${confidence}) - value estimate is reliable`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return considerations;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Format address from property data
|
|
281
|
+
*/
|
|
282
|
+
function formatAddress(property) {
|
|
283
|
+
const addr = property.address;
|
|
284
|
+
if (!addr)
|
|
285
|
+
return "Unknown Address";
|
|
286
|
+
const street = addr.street ||
|
|
287
|
+
[
|
|
288
|
+
addr.houseNumber,
|
|
289
|
+
addr.streetName,
|
|
290
|
+
addr.streetSuffix,
|
|
291
|
+
]
|
|
292
|
+
.filter(Boolean)
|
|
293
|
+
.join(" ");
|
|
294
|
+
const city = addr.city || "";
|
|
295
|
+
const state = addr.state || "";
|
|
296
|
+
const zip = addr.zip || "";
|
|
297
|
+
const formatted = `${street}, ${city}, ${state} ${zip}`.trim();
|
|
298
|
+
return formatted.replace(/[,\s]+$/, "") || "Unknown Address";
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Extract summary metrics from property data
|
|
302
|
+
*/
|
|
303
|
+
function extractSummary(property) {
|
|
304
|
+
const building = property.building;
|
|
305
|
+
const valuation = property.valuation;
|
|
306
|
+
const intel = property.intel;
|
|
307
|
+
const permit = property.permit;
|
|
308
|
+
const ownerProfile = property
|
|
309
|
+
.propertyOwnerProfile;
|
|
310
|
+
const owner = property.owner;
|
|
311
|
+
return {
|
|
312
|
+
estimatedValue: valuation?.estimatedValue,
|
|
313
|
+
equity: valuation?.equityCurrentEstimatedBalance,
|
|
314
|
+
equityPercent: valuation?.equityPercent,
|
|
315
|
+
ltv: valuation?.ltv,
|
|
316
|
+
salePropensity: intel?.salePropensity,
|
|
317
|
+
salePropensityCategory: intel?.salePropensityCategory,
|
|
318
|
+
yearBuilt: building?.yearBuilt,
|
|
319
|
+
sqft: building?.size ||
|
|
320
|
+
building?.livingSquareFeet,
|
|
321
|
+
bedrooms: building?.bedroomCount,
|
|
322
|
+
bathrooms: building?.bathCount,
|
|
323
|
+
propertyType: building?.generalPropertyType,
|
|
324
|
+
ownerName: owner?.fullName,
|
|
325
|
+
ownerType: owner?.type,
|
|
326
|
+
permitCount: permit?.permitCount,
|
|
327
|
+
ownerPortfolioSize: ownerProfile?.propertiesCount,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Extract domain-specific data from property
|
|
332
|
+
*/
|
|
333
|
+
function extractDomainData(property, domainName, options) {
|
|
334
|
+
const data = {};
|
|
335
|
+
const domain = exports.PROPERTY_DATA_DOMAINS.find((d) => d.name === domainName);
|
|
336
|
+
if (!domain)
|
|
337
|
+
return data;
|
|
338
|
+
// Get fields for this domain
|
|
339
|
+
const fields = (0, utils_1.getPropertyGroupFields)(domainName);
|
|
340
|
+
const keyFields = new Set(domain.keyFields || []);
|
|
341
|
+
// Prioritize key fields, then add others up to limit
|
|
342
|
+
const sortedFields = [...fields].sort((a, b) => {
|
|
343
|
+
const aKey = keyFields.has(a.fieldPath.split(".").pop() || "");
|
|
344
|
+
const bKey = keyFields.has(b.fieldPath.split(".").pop() || "");
|
|
345
|
+
if (aKey && !bKey)
|
|
346
|
+
return -1;
|
|
347
|
+
if (!aKey && bKey)
|
|
348
|
+
return 1;
|
|
349
|
+
return 0;
|
|
350
|
+
});
|
|
351
|
+
const limit = options.maxFieldsPerDomain || 20;
|
|
352
|
+
const fieldsToProcess = sortedFields.slice(0, limit);
|
|
353
|
+
for (const field of fieldsToProcess) {
|
|
354
|
+
const value = (0, utils_1.getPropertyFieldValue)(property, field.fieldPath);
|
|
355
|
+
if (value !== undefined && value !== null) {
|
|
356
|
+
const fieldName = field.fieldPath.split(".").pop() || field.fieldPath;
|
|
357
|
+
data[fieldName] = value;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return data;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Build comprehensive property data context for AI
|
|
364
|
+
* @param property The property object to analyze
|
|
365
|
+
* @param options Configuration options
|
|
366
|
+
* @returns Structured context for AI consumption
|
|
367
|
+
* @example
|
|
368
|
+
* ```typescript
|
|
369
|
+
* const context = buildPropertyDataContext(property);
|
|
370
|
+
* // Use context.summary for quick metrics
|
|
371
|
+
* // Use context.motivationSignals for seller motivation
|
|
372
|
+
* // Use context.considerations for investment analysis
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
function buildPropertyDataContext(property, options = {}) {
|
|
376
|
+
const selectedDomains = options.domains || exports.PROPERTY_DATA_DOMAINS.map((d) => d.name);
|
|
377
|
+
const domains = {};
|
|
378
|
+
for (const domainName of selectedDomains) {
|
|
379
|
+
const domainData = extractDomainData(property, domainName, options);
|
|
380
|
+
if (Object.keys(domainData).length > 0) {
|
|
381
|
+
domains[domainName] = domainData;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
address: formatAddress(property),
|
|
386
|
+
summary: extractSummary(property),
|
|
387
|
+
motivationSignals: extractMotivationSignals(property),
|
|
388
|
+
considerations: generateConsiderations(property),
|
|
389
|
+
domains,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Build AI system prompt section for property data context
|
|
394
|
+
* @param context The property data context
|
|
395
|
+
* @returns Formatted string for AI system prompt
|
|
396
|
+
*/
|
|
397
|
+
function formatPropertyContextForAI(context) {
|
|
398
|
+
const sections = [];
|
|
399
|
+
// Address
|
|
400
|
+
sections.push(`**Address:** ${context.address}`);
|
|
401
|
+
// Summary metrics
|
|
402
|
+
const s = context.summary;
|
|
403
|
+
sections.push("", "## Property Summary");
|
|
404
|
+
if (s.estimatedValue)
|
|
405
|
+
sections.push(`- Estimated Value: $${s.estimatedValue.toLocaleString()}`);
|
|
406
|
+
if (s.equity !== undefined)
|
|
407
|
+
sections.push(`- Equity: $${s.equity.toLocaleString()}`);
|
|
408
|
+
if (s.equityPercent !== undefined)
|
|
409
|
+
sections.push(`- Equity Percent: ${s.equityPercent.toFixed(1)}%`);
|
|
410
|
+
if (s.ltv !== undefined)
|
|
411
|
+
sections.push(`- LTV: ${s.ltv.toFixed(1)}%`);
|
|
412
|
+
if (s.salePropensity !== undefined)
|
|
413
|
+
sections.push(`- Sale Propensity: ${s.salePropensity}% (${s.salePropensityCategory || "Unknown"})`);
|
|
414
|
+
if (s.propertyType)
|
|
415
|
+
sections.push(`- Property Type: ${s.propertyType}`);
|
|
416
|
+
if (s.yearBuilt)
|
|
417
|
+
sections.push(`- Year Built: ${s.yearBuilt}`);
|
|
418
|
+
if (s.sqft)
|
|
419
|
+
sections.push(`- Square Feet: ${s.sqft.toLocaleString()}`);
|
|
420
|
+
if (s.bedrooms !== undefined)
|
|
421
|
+
sections.push(`- Bedrooms: ${s.bedrooms}`);
|
|
422
|
+
if (s.bathrooms !== undefined)
|
|
423
|
+
sections.push(`- Bathrooms: ${s.bathrooms}`);
|
|
424
|
+
if (s.ownerName)
|
|
425
|
+
sections.push(`- Owner: ${s.ownerName}`);
|
|
426
|
+
if (s.ownerType)
|
|
427
|
+
sections.push(`- Owner Type: ${s.ownerType}`);
|
|
428
|
+
if (s.permitCount)
|
|
429
|
+
sections.push(`- Permit Count: ${s.permitCount}`);
|
|
430
|
+
if (s.ownerPortfolioSize)
|
|
431
|
+
sections.push(`- Owner Portfolio Size: ${s.ownerPortfolioSize} properties`);
|
|
432
|
+
// Motivation signals
|
|
433
|
+
if (context.motivationSignals.length > 0) {
|
|
434
|
+
sections.push("", "## Motivation Signals");
|
|
435
|
+
for (const signal of context.motivationSignals) {
|
|
436
|
+
sections.push(`- ${signal}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Investment considerations
|
|
440
|
+
if (context.considerations.length > 0) {
|
|
441
|
+
sections.push("", "## Investment Considerations");
|
|
442
|
+
for (const consideration of context.considerations) {
|
|
443
|
+
sections.push(`- ${consideration}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return sections.join("\n");
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Get domain descriptions for AI context
|
|
450
|
+
* @param domainNames Optional list of domains to include
|
|
451
|
+
* @returns Formatted domain descriptions
|
|
452
|
+
*/
|
|
453
|
+
function getPropertyDomainDescriptions(domainNames) {
|
|
454
|
+
const selectedDomains = domainNames
|
|
455
|
+
? exports.PROPERTY_DATA_DOMAINS.filter((d) => domainNames.includes(d.name))
|
|
456
|
+
: exports.PROPERTY_DATA_DOMAINS;
|
|
457
|
+
const lines = ["## Property Data Domains", ""];
|
|
458
|
+
for (const domain of selectedDomains) {
|
|
459
|
+
lines.push(`**${domain.name}**: ${domain.description}`);
|
|
460
|
+
lines.push(` Investment Relevance: ${domain.investmentRelevance}`);
|
|
461
|
+
lines.push("");
|
|
462
|
+
}
|
|
463
|
+
return lines.join("\n");
|
|
464
|
+
}
|
package/package.json
CHANGED