UNPKG

@lineai/municipal-intel

Version:

AI-first municipal data API providing natural language descriptions of building permits and planning applications from major US cities

562 lines (438 loc) 16.9 kB
# @lineai/municipal-intel Access municipal planning applications, building permits, and construction activity data from major US cities. ## Overview `@lineai/municipal-intel` provides a unified interface to access local government data from major US cities. Built with TypeScript for maximum type safety, it features a hybrid architecture with built-in sources and runtime extensibility. ## Features ### AI-First Design - **Discovery API**: Comprehensive discovery methods for AI assistants - **Strong Typing**: TypeScript autocomplete for municipality IDs and search parameters - **Search Capabilities**: Dynamic capability detection per municipality - **Self-Documenting**: Rich metadata about available datasets and fields ### Data Access - **Unified API**: Single interface for multiple municipal data sources - **Real-time Data**: Access live permit and planning application data - **Clean Project URLs**: Each project includes a shareable URL for direct access - **URL-based Retrieval**: Get projects directly by URL for bookmarking and sharing - **Transparent Adjustments**: Best-effort queries with clear reporting of any modifications - **Schema Validation**: Zod schemas ensure data consistency and catch API changes ### Developer Experience - **TypeScript Native**: Built-in sources with full type safety - **Runtime Extensible**: Add new cities dynamically without package updates - **Universal Socrata Token**: Single token works across all Socrata portals - **Rate Limiting**: Built-in rate limiting and retry logic ## Supported Cities ### Built-in Sources (Ready to Use) - **California**: San Francisco, Los Angeles - **New York**: New York City (all 5 boroughs) ### Runtime Registration (Add Your Own) - **Any Socrata Portal**: Add any city with Socrata-based open data - **Custom APIs**: Register custom municipal APIs - **Extensible**: No package updates needed for new cities See [docs/ADD_NEW_SOURCE.md](./docs/ADD_NEW_SOURCE.md) for adding new sources. ## Installation ```bash npm install @lineai/municipal-intel ``` ## Quick Start ```typescript import { createMunicipalIntel } from '@lineai/municipal-intel'; // Create client instance const municipal = createMunicipalIntel({ debug: true }); // Set universal Socrata token (works for all cities) municipal.setSocrataToken(process.env.SOCRATA_TOKEN); // 🤖 AI Discovery: Find what's available const municipalities = municipal.getAvailableMunicipalities(); console.log('Available municipalities:', municipalities); // Check search capabilities for San Francisco const capabilities = municipal.getSearchCapabilities('sf'); console.log('SF supports:', capabilities.supportedFilters); // Search for construction permits in San Francisco const results = await municipal.search({ municipalityId: 'sf', // Type-safe municipality ID keywords: ['renovation'], minValue: 100000, limit: 10 }); console.log(`Found ${results.total} projects`); results.projects.forEach(project => { console.log(project.description); // Natural language description console.log('Project URL:', project.url); // Clean, shareable URL console.log('Raw data:', project.rawData); // Full original data }); // 🔗 Get project by URL (great for bookmarking/sharing) const projectUrl = 'https://municipal-intel.lineai.com/projects/sf/buildingPermits/2024-001'; const project = await municipal.getByUrl(projectUrl); if (project) { console.log('Retrieved project:', project.description); } // Check for any query adjustments if (results.adjustments.length > 0) { console.log('Query adjustments:', results.adjustments); } ``` ## Authentication ### Universal Socrata Token (Recommended) Get a **single** free app token that works across **all** Socrata portals: 1. Visit any Socrata portal (e.g., [data.sfgov.org](https://data.sfgov.org)) 2. Sign up Developer Settings Create App Token 3. Use this **same token** for all cities! ```typescript const municipal = createMunicipalIntel(); municipal.setSocrataToken(process.env.SOCRATA_TOKEN); ``` **Rate limits:** - **With token**: 1000 requests/hour per portal - **Without token**: Shared pool, very limited ## API Reference ### 🤖 AI Discovery Methods ```typescript // Get all available municipalities with their datasets const municipalities = municipal.getAvailableMunicipalities(); // Returns: [{ id: 'sf', name: 'San Francisco', state: 'CA', datasets: [...] }] // Check what search capabilities a municipality supports const capabilities = municipal.getSearchCapabilities('sf'); // Returns: { supportedFilters: ['minValue', 'submitDate', ...], limitations: [...] } // Get detailed field schema for a dataset const schema = municipal.getDatasetSchema('sf', 'buildingPermits'); // Returns: [{ name: 'permit_number', type: 'string', searchable: true }] ``` ### Search Projects ```typescript const results = await municipal.search({ // Location filters municipalityId: 'sf', // Type-safe municipality ('sf' | 'nyc' | 'la') addresses: ['Market Street'], // By address zipCodes: ['94102'], // By ZIP code // Project filters types: ['permit', 'planning'], // Project types statuses: ['approved', 'issued'], // Current status keywords: ['renovation'], // Keywords // Date filters submitDateFrom: new Date('2024-01-01'), submitDateTo: new Date('2024-12-31'), // Value filters (if supported by municipality) minValue: 50000, maxValue: 1000000, // Pagination limit: 50, offset: 0, // Sorting sortBy: 'submitDate', sortOrder: 'desc' }); // Check for query adjustments console.log('Adjustments:', results.adjustments); ``` ### Get Specific Project ```typescript const project = await municipal.getProject('sf', 'sf-202401234567'); ``` ### List Available Sources ```typescript // All sources const allSources = municipal.getSources(); // Filter by criteria const apiSources = municipal.getSources({ type: 'api', priority: 'high' }); // By state const caSources = municipal.getSources({ state: 'ca' }); ``` ### Health Checks ```typescript const health = await municipal.healthCheck('sf'); console.log(`Status: ${health.status}, Latency: ${health.latency}ms`); ``` ### Runtime Source Registration Add new cities without updating the package: ```typescript // Register a new Florida city municipal.registerSource({ id: 'miami', name: 'Miami-Dade County', state: 'FL', type: 'api', api: { type: 'socrata', baseUrl: 'https://opendata.miamidade.gov', datasets: { buildingPermits: { endpoint: '/resource/8wbx-tpnc.json', name: 'Building Permits', fields: ['permit_number', 'status', 'issue_date', 'address', 'valuation', 'description'], fieldMappings: { id: 'permit_number', status: 'status', submitDate: 'issue_date', address: 'address', value: 'valuation', description: 'description' } } } }, priority: 'high' }); // Now you can search Miami data const miamiResults = await municipal.search({ municipalityId: 'miami' }); // Remove runtime source municipal.unregisterSource('miami'); ``` ## Query Adjustments & Transparency The library implements a best-effort approach with full transparency when handling different data sources. When a requested filter cannot be applied to a particular source, the library will continue the search and report what adjustments were made. ### Adjustments Array All search responses include an `adjustments` array that reports any modifications made to your query: ```typescript interface MunicipalSearchResponse { projects: MunicipalProject[]; total: number; page: number; pageSize: number; hasMore: boolean; adjustments: string[]; // Query modifications made during search } ``` ### Common Adjustments **Value Filter Skipping**: Some sources don't have cost/value fields ```typescript const results = await municipal.search({ sources: ['sf', 'nyc'], minValue: 100000 }); // Results: // { // projects: [...], // total: 1234, // adjustments: [ // "NYC: Skipped minValue filter - no value field available in dataset" // ] // } ``` **Field Type Conversions**: Socrata sources store numbers as text, requiring automatic casting ```typescript // The library automatically converts text to numbers for comparisons // SF: estimated_cost field contains "500000" (string) // Query: WHERE estimated_cost::number >= 100000 // No adjustment needed - handled transparently ``` ### Adjustment Principles - **Silent Success**: No adjustments reported when everything works normally - **Transparent Failures**: Only report when something unexpected happens - **Continue on Errors**: Skip problematic filters rather than fail entire search - **AI-Friendly**: Clean responses for automated processing ### Best Practices **For AI Assistants**: ```typescript const results = await municipal.search(params); if (results.adjustments.length > 0) { // Inform user about limitations console.log('Note: ' + results.adjustments.join('; ')); } ``` **For Applications**: ```typescript // Always check adjustments for user feedback if (results.adjustments.length > 0) { showWarning('Some filters were not available for all sources'); } ``` ## Data Types ### MunicipalProject (AI-First Design) ```typescript interface MunicipalProject { // Core fields optimized for AI consumption id: string; // Unique identifier (e.g., "sf-2024-001234") source: string; // Source municipality ID (e.g., "sf", "nyc") description: string; // Natural language description with full context rawData: any; // Complete original API response lastUpdated: Date; // When data was last fetched } ``` **Key Design Principles:** - **AI-Optimized**: Natural language descriptions instead of failed field normalization - **Complete Context**: Descriptions include full geographic and municipal context - **Raw Data Preserved**: Full original API response available for detailed analysis - **Lightweight**: Reduced payload size by ~70% vs. previous complex schema ## Examples ### Monitor New Permits ```typescript // Get recent permits from San Francisco const recent = await municipal.search({ municipalityId: 'sf', types: ['permit'], submitDateFrom: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // Last 7 days sortBy: 'submitDate', sortOrder: 'desc' }); // AI-friendly output with natural language descriptions recent.projects.forEach(project => { console.log(project.description); // Example: "Residential alteration permit for kitchen renovation at 123 Main St, San Francisco, CA 94102. Filed 2024-01-15, issued 2024-02-01. Estimated cost: $75,000." }); // Monitor multiple municipalities const municipalities = municipal.getAvailableMunicipalities(); for (const municipality of municipalities) { const permits = await municipal.search({ municipalityId: municipality.id, limit: 5 }); console.log(`\n${municipality.name}: ${permits.total} recent projects`); permits.projects.forEach(p => console.log(` ${p.description}`)); } ``` ### Construction Projects by Value ```typescript // High-value construction projects in SF const bigProjects = await municipal.search({ municipalityId: 'sf', minValue: 1000000, sortBy: 'value', sortOrder: 'desc' }); // Natural language descriptions include value context automatically bigProjects.projects.forEach(project => { console.log(project.description); // Example: "New construction permit for 45-unit residential building at 456 Market St, San Francisco, CA 94105. Filed 2024-01-10, approved 2024-03-15. Estimated cost: $12,500,000. Developer: XYZ Development Corp." // Access raw data for detailed analysis const rawValue = project.rawData.estimated_cost; const submitter = project.rawData.applicant_name; }); // Check capabilities const municipalities = municipal.getAvailableMunicipalities(); for (const municipality of municipalities) { const capabilities = municipal.getSearchCapabilities(municipality.id); if (capabilities.supportedFilters.includes('minValue')) { console.log(`${municipality.name} supports value filtering`); } } ``` ### Geographic Search ```typescript // Projects in specific SF area const localProjects = await municipal.search({ municipalityId: 'sf', addresses: ['Market Street', 'Mission Street'], zipCodes: ['94102', '94103'], limit: 20 }); // Descriptions include full geographic context localProjects.projects.forEach(project => { console.log(project.description); // Example: "Commercial tenant improvement at 789 Mission St, San Francisco, CA 94103. Restaurant buildout permit filed 2024-02-20, under review. Estimated cost: $150,000." }); // Cross-municipality geographic search const addressSearch = async (streetName: string) => { const municipalities = municipal.getAvailableMunicipalities(); for (const municipality of municipalities) { const results = await municipal.search({ municipalityId: municipality.id, addresses: [streetName], limit: 3 }); console.log(`\n${municipality.name}: ${results.total} projects on ${streetName}`); results.projects.forEach(p => { console.log(` ${p.description}`); }); } }; await addressSearch('Main Street'); ``` ## Environment Variables ```bash # .env SOCRATA_TOKEN=your-universal-socrata-app-token ``` **Note**: A single Socrata token works across **all** Socrata portals (San Francisco, NYC, Miami, etc.) ## Error Handling ```typescript import { MunicipalDataError, RateLimitError } from '@lineai/municipal-intel'; try { const results = await municipal.search({ sources: ['sf'] }); } catch (error) { if (error instanceof RateLimitError) { console.log('Rate limited, retry after:', error.details?.resetTime); } else if (error instanceof MunicipalDataError) { console.log('API error:', error.message, 'Source:', error.source); } } ``` ## Troubleshooting ### Value Filtering Issues **Problem**: Getting 400 errors when using `minValue` parameter ```typescript // This might fail with "Invalid SoQL query" error const results = await municipal.search({ municipalityId: 'sf', minValue: 100000 // 400 error }); ``` **Solution**: The library automatically handles this by converting text fields to numbers. If you see this error, ensure you're using the latest version. **Explanation**: Socrata APIs store numeric values as text strings (e.g., `"500000"` instead of `500000`). The library automatically applies `::number` casting for all numeric comparisons. ### Missing Value Fields **Problem**: Some sources don't return value information ```typescript const results = await municipal.search({ municipalityId: 'nyc', minValue: 100000 }); // NYC projects show value: null ``` **Solution**: Check the `adjustments` array for information about skipped filters: ```typescript if (results.adjustments.length > 0) { console.log('Filters adjusted:', results.adjustments); // Output: ["NYC: Skipped minValue filter - no value field available in dataset"] } ``` ### Rate Limiting **Problem**: Getting rate limited frequently **Solution**: Use a Socrata app token: ```typescript municipal.setSocrataToken(process.env.SOCRATA_TOKEN); // Increases limit from ~100/hour to 1000/hour per portal ``` ### Field Mappings **Problem**: Custom sources not returning expected data **Solution**: Verify field mappings match actual API response: 1. Check the API samples in `/api-samples/` directory 2. Ensure your `fieldMappings` use correct field names at the dataset level 3. Use the library's field discovery tools ```typescript // Check available fields for your source const source = municipal.getSource('your-city'); console.log('Available fields:', source.api?.datasets?.buildingPermits?.fields); // Check what fields are mapped const fieldMappings = source.api?.datasets?.buildingPermits?.fieldMappings; console.log('Field mappings:', fieldMappings); // Use schema discovery to validate const schema = municipal.getDatasetSchema('your-city', 'buildingPermits'); console.log('Schema:', schema); ``` ## Development ```bash # Install dependencies yarn install # Build yarn build # Test yarn test:unit # Lint and fix yarn fix ``` ## Contributing 1. Fork the repository 2. Create a feature branch 3. Add tests for new functionality 4. Ensure all tests pass 5. Submit a pull request ## Documentation - [AI-First Design Philosophy](./docs/AI_FIRST_DESIGN.md) - Complete guide to the natural language description approach - [Adding New Sources](./docs/ADD_NEW_SOURCE.md) - How to add new cities and data sources - [API Guide](./docs/MUNICIPAL_API_GUIDE.md) - Technical implementation details ## License MIT ## Related Packages - [@lineai/gov-deals](https://www.npmjs.com/package/@lineai/gov-deals) - Federal government contracts and opportunities