Skip to content

Commit 96c04cf

Browse files
authored
Merge pull request #57 from codeforpakistan/feature/they-work-foryou
Feature: Find your numainda
2 parents 90d8e0b + a4ebfdd commit 96c04cf

35 files changed

+10397
-3036
lines changed

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@ yarn-error.log*
3535
.contentlayer
3636
.env
3737

38-
docs/
38+
docs/
39+
data/
40+
.playwright-mcp/
41+
42+
# Representative images (generated from data/)
43+
public/representatives/

app/api/chat/route.tsx

Lines changed: 98 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { openai } from "@ai-sdk/openai"
22
import { streamText } from "ai"
33

4-
import { findRelevantContent } from "@/lib/ai/embedding"
4+
import { findRelevantContent, findRelevantRepresentatives, detectQueryTypes } from "@/lib/ai/embedding"
55

66
export const maxDuration = 30
77

@@ -10,69 +10,136 @@ export async function POST(req: Request) {
1010

1111
// Get relevant content for the last message
1212
const lastMessage = messages[messages.length - 1]
13-
const relevantContent = await findRelevantContent(lastMessage.content)
14-
15-
// Format the content for the AI to use
16-
const contextString = relevantContent
17-
.map((content) => {
18-
return `Document: ${content.documentTitle}
19-
Type: ${content.documentType}
20-
Content: ${content.content}
21-
---`
22-
})
23-
.join("\n\n")
13+
14+
// Use AI to detect query types (can be multiple)
15+
const queryTypes = await detectQueryTypes(lastMessage.content)
16+
17+
const contextParts: string[] = []
18+
19+
// Search all relevant content types in parallel
20+
const searchPromises = []
21+
22+
if (queryTypes.includes('representative')) {
23+
searchPromises.push(
24+
findRelevantRepresentatives(lastMessage.content).then((reps) => ({
25+
type: 'representative' as const,
26+
data: reps,
27+
}))
28+
)
29+
}
30+
31+
if (queryTypes.includes('bill') || queryTypes.includes('document')) {
32+
searchPromises.push(
33+
findRelevantContent(lastMessage.content).then((docs) => ({
34+
type: 'document' as const,
35+
data: docs,
36+
}))
37+
)
38+
}
39+
40+
// Execute searches in parallel
41+
const results = await Promise.all(searchPromises)
42+
43+
// Format results
44+
for (const result of results) {
45+
if (result.type === 'representative' && result.data.length > 0) {
46+
const repContext = result.data
47+
.map((rep) => {
48+
return `Representative: ${rep.name}
49+
Constituency: ${rep.constituencyCode} - ${rep.constituencyName || rep.constituency}
50+
District: ${rep.district || 'N/A'}
51+
Province: ${rep.province}
52+
Party: ${rep.party}
53+
Phone: ${rep.phone || 'Not available'}
54+
Permanent Address: ${rep.permanentAddress || 'Not available'}
55+
Islamabad Address: ${rep.islamabadAddress || 'Not available'}
56+
---`
57+
})
58+
.join("\n\n")
59+
contextParts.push(repContext)
60+
}
61+
62+
if (result.type === 'document' && result.data.length > 0) {
63+
const docContext = result.data
64+
.map((content) => {
65+
return `Document: ${content.documentTitle}
66+
Type: ${content.documentType}
67+
Content: ${content.content}
68+
---`
69+
})
70+
.join("\n\n")
71+
contextParts.push(docContext)
72+
}
73+
}
74+
75+
const contextString = contextParts.join("\n\n=== DIFFERENT SOURCE TYPE ===\n\n")
2476

2577
const result = streamText({
2678
model: openai("gpt-4o-mini"),
2779
messages,
28-
system: `You are Numainda, an AI assistant designed to share insights and facts derived exclusively from Pakistan's Constitution, Elections Act 2017, parliamentary proceedings, and National Assembly bills. Your purpose is to make Pakistan's legislative framework accessible and engaging.
80+
system: `You are Numainda, an AI assistant designed to help Pakistani citizens connect with their National Assembly representatives and understand Pakistan's Constitution, Elections Act 2017, parliamentary proceedings, and bills. Your purpose is to make Pakistan's legislative framework and representatives accessible to all.
81+
82+
Here is the relevant information to help answer the question:
2983
30-
Here is the relevant information from official documents to help answer the question:
31-
3284
${contextString}
33-
85+
3486
Core Instructions:
3587
1. Base your responses EXCLUSIVELY on the provided information above. Never venture into speculative or inferred information not directly available from the sources.
36-
88+
3789
2. Response Structure:
38-
- Begin by citing your source document(s)
90+
- Begin by citing your source (document or representative name)
91+
- For representatives: Include name, constituency, party, and contact information
3992
- For bills: Include bill status, passage date (if passed), and key provisions
4093
- Use clear, simple language that's accessible to all
4194
- Incorporate relevant emojis to enhance readability
42-
- Add appropriate hashtags (e.g., #PakistanLaws, #NABill, #PakParliament)
43-
44-
3. When discussing bills:
95+
- Add appropriate hashtags (e.g., #PakistanLaws, #NABill, #PakParliament, #YourRepresentative)
96+
97+
3. When discussing representatives:
98+
- Clearly present the representative's name and constituency
99+
- Include their political party affiliation
100+
- Provide contact information (phone, addresses) if available
101+
- Use format: "👤 [Name] represents [Constituency] ([Party])"
102+
- Always encourage citizens to reach out to their representatives
103+
104+
4. For cross-entity queries (e.g., "Did representative X present a bill?"):
105+
- You may receive BOTH representative AND document information
106+
- Synthesize information from multiple source types
107+
- Connect representatives to bills/legislation when relevant
108+
- Be transparent if connection cannot be established from provided data
109+
- Example: "Based on the information provided, I can tell you about [Rep Name] and these bills, but I cannot confirm direct authorship/sponsorship"
110+
111+
5. When discussing bills:
45112
- Clearly state the bill's current status (pending/passed/rejected)
46113
- Highlight main objectives and key provisions
47114
- If passed, mention the passage date and implementation timeline
48115
- Explain potential impacts on citizens or institutions
49116
- Use format: "Bill Title (Status): Key Points"
50-
51-
4. For questions without relevant information:
117+
118+
6. For questions without relevant information:
52119
- Respond: "I don't have sufficient information in the provided documents to answer this question."
53-
- Suggest related bills or legislation you do have information about
120+
- Suggest related bills, legislation, or representatives you do have information about
54121
- Maintain transparency about knowledge limitations
55-
56-
5. When synthesizing multiple sources:
122+
123+
7. When synthesizing multiple sources:
57124
- Present information chronologically or by relevance
58125
- Show relationships between bills and existing laws
59126
- Highlight any amendments or changes to existing legislation
60127
- Use direct quotes sparingly and only for crucial details
61-
62-
6. Special Content Types:
128+
129+
8. Special Content Types:
63130
If asked for a "tweet":
64131
- Create engaging, fact-based content within 280 characters
65132
- Include source attribution and bill status for legislation
66133
- Use emojis and hashtags appropriately
67134
- Example: "📜 New Bill Alert! The [Bill Name] aims to [main objective]. Current status: [Status] 🏛️ #PakParliament"
68-
69-
7. Tone and Style:
135+
136+
9. Tone and Style:
70137
- Maintain a balance between authoritative and engaging
71138
- Use formal language for legislative matters
72139
- Add appropriate emojis and hashtags to enhance engagement
73140
- Keep responses clear, concise, and educational
74141
75-
8. Do not hallucinate or speculate:
142+
10. Do not hallucinate or speculate:
76143
- Stick strictly to information in the provided documents
77144
- For bills: Only discuss provisions explicitly stated
78145
- If asked about implementation details not in the text, acknowledge the limitation
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { NextResponse } from 'next/server';
2+
import { db } from '@/lib/db';
3+
import { representatives } from '@/lib/db/schema/representatives';
4+
import { eq } from 'drizzle-orm';
5+
6+
export const dynamic = 'force-dynamic';
7+
8+
export async function GET(
9+
request: Request,
10+
{ params }: { params: { id: string } }
11+
) {
12+
try {
13+
const { id } = params;
14+
15+
// Fetch representative by ID
16+
const rep = await db
17+
.select()
18+
.from(representatives)
19+
.where(eq(representatives.id, id))
20+
.limit(1);
21+
22+
if (rep.length === 0) {
23+
return NextResponse.json(
24+
{ error: 'Representative not found' },
25+
{ status: 404 }
26+
);
27+
}
28+
29+
// Transform imageLocalPath to public URL
30+
const repWithImage = {
31+
...rep[0],
32+
imageUrl: rep[0].imageLocalPath
33+
? `/representatives/${rep[0].imageLocalPath.split('/').pop()}`
34+
: rep[0].imageUrl,
35+
};
36+
37+
return NextResponse.json({
38+
data: repWithImage,
39+
});
40+
} catch (error) {
41+
console.error('Failed to fetch representative:', error);
42+
return NextResponse.json(
43+
{ error: 'Failed to fetch representative' },
44+
{ status: 500 }
45+
);
46+
}
47+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { NextResponse } from 'next/server';
2+
import { db } from '@/lib/db';
3+
import { representatives } from '@/lib/db/schema/representatives';
4+
import { sql, isNotNull } from 'drizzle-orm';
5+
6+
export const dynamic = 'force-dynamic';
7+
8+
/**
9+
* Find representatives by location (lat/lng)
10+
* Uses Haversine formula to calculate distance
11+
*
12+
* Query params:
13+
* - lat: latitude (required)
14+
* - lng: longitude (required)
15+
* - radius: search radius in km (default: 50)
16+
* - limit: max results (default: 10)
17+
*/
18+
export async function GET(request: Request) {
19+
try {
20+
const { searchParams } = new URL(request.url);
21+
const lat = parseFloat(searchParams.get('lat') || '');
22+
const lng = parseFloat(searchParams.get('lng') || '');
23+
const radius = parseFloat(searchParams.get('radius') || '50'); // km
24+
const limit = parseInt(searchParams.get('limit') || '10', 10);
25+
26+
if (isNaN(lat) || isNaN(lng)) {
27+
return NextResponse.json(
28+
{ error: 'Valid lat and lng parameters are required' },
29+
{ status: 400 }
30+
);
31+
}
32+
33+
// Use subquery to calculate distance, then filter
34+
const results = await db.execute(sql`
35+
SELECT * FROM (
36+
SELECT
37+
id,
38+
name,
39+
name_clean as "nameClean",
40+
constituency,
41+
constituency_code as "constituencyCode",
42+
constituency_name as "constituencyName",
43+
district,
44+
province,
45+
party,
46+
image_url as "imageUrl",
47+
image_local_path as "imageLocalPath",
48+
phone,
49+
latitude,
50+
longitude,
51+
permanent_address as "permanentAddress",
52+
islamabad_address as "islamabadAddress",
53+
(
54+
6371 * acos(
55+
cos(radians(${lat}))
56+
* cos(radians(latitude))
57+
* cos(radians(longitude) - radians(${lng}))
58+
+ sin(radians(${lat}))
59+
* sin(radians(latitude))
60+
)
61+
) as distance
62+
FROM representatives
63+
WHERE latitude IS NOT NULL
64+
) as with_distance
65+
WHERE distance <= ${radius}
66+
ORDER BY distance
67+
LIMIT ${limit}
68+
`);
69+
70+
// Transform imageLocalPath to public URL
71+
const resultsWithImages = results.map((rep: any) => ({
72+
...rep,
73+
imageUrl: rep.imageLocalPath
74+
? `/representatives/${rep.imageLocalPath.split('/').pop()}`
75+
: rep.imageUrl,
76+
}));
77+
78+
return NextResponse.json({
79+
data: resultsWithImages,
80+
query: {
81+
lat,
82+
lng,
83+
radius,
84+
},
85+
});
86+
} catch (error) {
87+
console.error('Failed to fetch representatives by location:', error);
88+
return NextResponse.json(
89+
{ error: 'Failed to fetch representatives by location' },
90+
{ status: 500 }
91+
);
92+
}
93+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { NextResponse } from 'next/server';
2+
import { db } from '@/lib/db';
3+
import { representatives } from '@/lib/db/schema/representatives';
4+
import { sql } from 'drizzle-orm';
5+
6+
export const dynamic = 'force-dynamic';
7+
8+
/**
9+
* Get available filter options for representatives
10+
* Returns unique values for provinces, parties, and districts
11+
*/
12+
export async function GET() {
13+
try {
14+
// Get unique provinces
15+
const provincesResult = await db
16+
.selectDistinct({ province: representatives.province })
17+
.from(representatives)
18+
.where(sql`${representatives.province} IS NOT NULL`)
19+
.orderBy(representatives.province);
20+
21+
// Get unique parties
22+
const partiesResult = await db
23+
.selectDistinct({ party: representatives.party })
24+
.from(representatives)
25+
.where(sql`${representatives.party} IS NOT NULL`)
26+
.orderBy(representatives.party);
27+
28+
// Get unique districts
29+
const districtsResult = await db
30+
.selectDistinct({ district: representatives.district })
31+
.from(representatives)
32+
.where(sql`${representatives.district} IS NOT NULL`)
33+
.orderBy(representatives.district);
34+
35+
const provinces = provincesResult
36+
.map((r) => r.province)
37+
.filter((p): p is string => p !== null);
38+
39+
const parties = partiesResult
40+
.map((r) => r.party)
41+
.filter((p): p is string => p !== null);
42+
43+
const districts = districtsResult
44+
.map((r) => r.district)
45+
.filter((d): d is string => d !== null);
46+
47+
return NextResponse.json({
48+
data: {
49+
provinces,
50+
parties,
51+
districts,
52+
},
53+
});
54+
} catch (error) {
55+
console.error('Failed to fetch filter options:', error);
56+
return NextResponse.json(
57+
{ error: 'Failed to fetch filter options' },
58+
{ status: 500 }
59+
);
60+
}
61+
}

0 commit comments

Comments
 (0)