georag 0.1.0__tar.gz
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.
- georag-0.1.0/PKG-INFO +13 -0
- georag-0.1.0/README.md +1 -0
- georag-0.1.0/pyproject.toml +15 -0
- georag-0.1.0/setup.cfg +4 -0
- georag-0.1.0/src/georag/__init__.py +54 -0
- georag-0.1.0/src/georag/cluster.py +47 -0
- georag-0.1.0/src/georag/models.py +38 -0
- georag-0.1.0/src/georag/parsers.py +42 -0
- georag-0.1.0/src/georag/retrievers.py +36 -0
- georag-0.1.0/src/georag.egg-info/PKG-INFO +13 -0
- georag-0.1.0/src/georag.egg-info/SOURCES.txt +12 -0
- georag-0.1.0/src/georag.egg-info/dependency_links.txt +1 -0
- georag-0.1.0/src/georag.egg-info/requires.txt +4 -0
- georag-0.1.0/src/georag.egg-info/top_level.txt +1 -0
georag-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: georag
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Spatial-Semantic Retrieval Augmented Generation framework
|
|
5
|
+
Author-email: Developer <makindeadedeji500@gmail.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: django>=4.2
|
|
9
|
+
Requires-Dist: scikit-learn>=1.3.0
|
|
10
|
+
Requires-Dist: psycopg2-binary>=2.9
|
|
11
|
+
Requires-Dist: numpy>=1.24
|
|
12
|
+
|
|
13
|
+
# georag
|
georag-0.1.0/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# georag
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "georag"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Spatial-Semantic Retrieval Augmented Generation framework"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Developer", email = "makindeadedeji500@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"django>=4.2",
|
|
12
|
+
"scikit-learn>=1.3.0",
|
|
13
|
+
"psycopg2-binary>=2.9",
|
|
14
|
+
"numpy>=1.24"
|
|
15
|
+
]
|
georag-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Dict, Any
|
|
2
|
+
from .parsers import GeoParser
|
|
3
|
+
from .retrievers import SpatialVectorRetriever
|
|
4
|
+
from .cluster import SpatialRouter
|
|
5
|
+
|
|
6
|
+
class GeoRAGPipeline:
|
|
7
|
+
"""
|
|
8
|
+
The main orchestration engine for the GeoRAG framework.
|
|
9
|
+
"""
|
|
10
|
+
def __init__(self, chunk_model, max_clusters: int = 3):
|
|
11
|
+
self.chunk_model = chunk_model
|
|
12
|
+
self.retriever = SpatialVectorRetriever(chunk_model)
|
|
13
|
+
self.router = SpatialRouter(max_clusters=max_clusters)
|
|
14
|
+
|
|
15
|
+
def generate_context(
|
|
16
|
+
self,
|
|
17
|
+
query_embedding: list[float],
|
|
18
|
+
target_longitude: float,
|
|
19
|
+
target_latitude: float,
|
|
20
|
+
radius_km: float,
|
|
21
|
+
top_k: int = 10
|
|
22
|
+
) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Executes the full pipeline and formats the results into an LLM-ready context string.
|
|
25
|
+
"""
|
|
26
|
+
# 1. Parse coordinates into PostGIS format
|
|
27
|
+
target_point = GeoParser.point_from_coords(target_longitude, target_latitude)
|
|
28
|
+
|
|
29
|
+
# 2. Run the two-pass spatial-semantic search
|
|
30
|
+
results = self.retriever.hybrid_search(
|
|
31
|
+
query_embedding=query_embedding,
|
|
32
|
+
target_point=target_point,
|
|
33
|
+
radius_km=radius_km,
|
|
34
|
+
top_k=top_k
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# 3. Route and cluster the data geographically
|
|
38
|
+
clustered_data = self.router.cluster_results(results)
|
|
39
|
+
|
|
40
|
+
# 4. Synthesize the context string
|
|
41
|
+
if not clustered_data:
|
|
42
|
+
return "No relevant geospatial data found for this query."
|
|
43
|
+
|
|
44
|
+
context_string = "Geospatial Data Context:\n"
|
|
45
|
+
for region_id, data in clustered_data.items():
|
|
46
|
+
lon, lat = data['centroid_lon_lat']
|
|
47
|
+
context_string += f"\n--- {region_id} (Approx. Center: {lon:.4f} Lon, {lat:.4f} Lat) ---\n"
|
|
48
|
+
for excerpt in data['excerpts']:
|
|
49
|
+
context_string += f"- {excerpt}\n"
|
|
50
|
+
|
|
51
|
+
return context_string
|
|
52
|
+
|
|
53
|
+
# Expose key components at the package level
|
|
54
|
+
__all__ = ['GeoRAGPipeline', 'GeoParser']
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from sklearn.cluster import KMeans
|
|
3
|
+
|
|
4
|
+
class SpatialRouter:
|
|
5
|
+
"""
|
|
6
|
+
Groups retrieved geographic chunks into logical spatial clusters
|
|
7
|
+
to provide structured context for the LLM.
|
|
8
|
+
"""
|
|
9
|
+
def __init__(self, max_clusters: int = 3):
|
|
10
|
+
self.max_clusters = max_clusters
|
|
11
|
+
|
|
12
|
+
def cluster_results(self, chunks) -> dict:
|
|
13
|
+
"""
|
|
14
|
+
Takes a QuerySet of retrieved chunk objects and groups them geographically.
|
|
15
|
+
Returns a dictionary mapping cluster IDs to their centroid coordinates and text.
|
|
16
|
+
"""
|
|
17
|
+
# Convert QuerySet to a list to avoid multiple database hits
|
|
18
|
+
chunk_list = list(chunks)
|
|
19
|
+
|
|
20
|
+
if not chunk_list:
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
# Extract coordinates [longitude, latitude] for the clustering algorithm
|
|
24
|
+
coords = np.array([[chunk.geometry.x, chunk.geometry.y] for chunk in chunk_list])
|
|
25
|
+
|
|
26
|
+
# We can't have more clusters than we have data points
|
|
27
|
+
n_clusters = min(self.max_clusters, len(chunk_list))
|
|
28
|
+
|
|
29
|
+
# Initialize and run Scikit-Learn K-Means
|
|
30
|
+
kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
|
|
31
|
+
labels = kmeans.fit_predict(coords)
|
|
32
|
+
|
|
33
|
+
grouped_data = {}
|
|
34
|
+
for idx, label in enumerate(labels):
|
|
35
|
+
cluster_id = f"Region_{label + 1}"
|
|
36
|
+
|
|
37
|
+
if cluster_id not in grouped_data:
|
|
38
|
+
# Store the geometric center of this cluster
|
|
39
|
+
centroid = kmeans.cluster_centers_[label].tolist()
|
|
40
|
+
grouped_data[cluster_id] = {
|
|
41
|
+
"centroid_lon_lat": centroid,
|
|
42
|
+
"excerpts": []
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
grouped_data[cluster_id]["excerpts"].append(chunk_list[idx].text)
|
|
46
|
+
|
|
47
|
+
return grouped_data
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from django.contrib.gis.db import models as gis_models
|
|
2
|
+
from django.db import models
|
|
3
|
+
from pgvector.django import VectorField, HnswIndex
|
|
4
|
+
from django.contrib.postgres.indexes import GistIndex
|
|
5
|
+
|
|
6
|
+
class GeoRAGDocumentBase(models.Model):
|
|
7
|
+
"""
|
|
8
|
+
Abstract base model representing a parent document.
|
|
9
|
+
"""
|
|
10
|
+
title = models.CharField(max_length=255)
|
|
11
|
+
metadata = models.JSONField(default=dict, blank=True)
|
|
12
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
13
|
+
|
|
14
|
+
class Meta:
|
|
15
|
+
abstract = True
|
|
16
|
+
|
|
17
|
+
class GeoRAGChunkBase(models.Model):
|
|
18
|
+
"""
|
|
19
|
+
Abstract base model representing a text chunk with spatial and semantic data.
|
|
20
|
+
"""
|
|
21
|
+
text = models.TextField()
|
|
22
|
+
embedding = VectorField(dimensions=768, null=True, blank=True)
|
|
23
|
+
geometry = gis_models.GeometryField(srid=4326, null=True, blank=True)
|
|
24
|
+
|
|
25
|
+
class Meta:
|
|
26
|
+
abstract = True
|
|
27
|
+
indexes = [
|
|
28
|
+
# Optimize spatial queries (radius, intersection)
|
|
29
|
+
GistIndex(fields=['geometry'], name='%(class)s_geom_idx'),
|
|
30
|
+
# Optimize semantic vector search (cosine distance)
|
|
31
|
+
HnswIndex(
|
|
32
|
+
name='%(class)s_vector_idx',
|
|
33
|
+
fields=['embedding'],
|
|
34
|
+
m=16,
|
|
35
|
+
ef_construction=64,
|
|
36
|
+
opclasses=['vector_cosine_ops']
|
|
37
|
+
)
|
|
38
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from django.contrib.gis.geos import GEOSGeometry, Point
|
|
3
|
+
|
|
4
|
+
class GeoParser:
|
|
5
|
+
"""
|
|
6
|
+
Handles the conversion of raw coordinates or GeoJSON into PostGIS-compatible geometries.
|
|
7
|
+
"""
|
|
8
|
+
@staticmethod
|
|
9
|
+
def point_from_coords(longitude: float, latitude: float) -> Point:
|
|
10
|
+
"""
|
|
11
|
+
Creates a standard PostGIS Point geometry.
|
|
12
|
+
CRITICAL: In spatial databases, Longitude (X) always comes before Latitude (Y).
|
|
13
|
+
"""
|
|
14
|
+
# SRID 4326 is the standard GPS coordinate system (WGS 84)
|
|
15
|
+
return Point(longitude, latitude, srid=4326)
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def parse_geojson(geojson_dict: dict) -> GEOSGeometry:
|
|
19
|
+
"""
|
|
20
|
+
Converts a standard GeoJSON boundary (like a polygon of an oil spill) into PostGIS geometry.
|
|
21
|
+
"""
|
|
22
|
+
return GEOSGeometry(json.dumps(geojson_dict))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DocumentChunker:
|
|
26
|
+
"""
|
|
27
|
+
Splits large documents into smaller chunks for vector embedding,
|
|
28
|
+
preparing them to be tagged with spatial metadata.
|
|
29
|
+
"""
|
|
30
|
+
def __init__(self, chunk_size: int = 250):
|
|
31
|
+
self.chunk_size = chunk_size
|
|
32
|
+
|
|
33
|
+
def chunk_text(self, text: str) -> list[str]:
|
|
34
|
+
"""
|
|
35
|
+
A robust word-based chunker.
|
|
36
|
+
"""
|
|
37
|
+
words = text.split()
|
|
38
|
+
chunks = []
|
|
39
|
+
for i in range(0, len(words), self.chunk_size):
|
|
40
|
+
chunk = " ".join(words[i:i + self.chunk_size])
|
|
41
|
+
chunks.append(chunk)
|
|
42
|
+
return chunks
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from django.contrib.gis.geos import Point
|
|
2
|
+
from django.contrib.gis.measure import Distance
|
|
3
|
+
from pgvector.django import CosineDistance
|
|
4
|
+
from django.db.models import QuerySet
|
|
5
|
+
|
|
6
|
+
class SpatialVectorRetriever:
|
|
7
|
+
"""
|
|
8
|
+
Executes the two-pass GeoRAG algorithm combining PostGIS spatial boundaries
|
|
9
|
+
with pgvector semantic cosine similarity.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, chunk_model):
|
|
12
|
+
"""
|
|
13
|
+
Initializes the retriever with the specific Django model to query.
|
|
14
|
+
"""
|
|
15
|
+
self.chunk_model = chunk_model
|
|
16
|
+
|
|
17
|
+
def hybrid_search(
|
|
18
|
+
self,
|
|
19
|
+
query_embedding: list[float],
|
|
20
|
+
target_point: Point,
|
|
21
|
+
radius_km: float,
|
|
22
|
+
top_k: int = 5
|
|
23
|
+
) -> QuerySet:
|
|
24
|
+
"""
|
|
25
|
+
Pass 1: Filter database by geographic radius using GiST indexes.
|
|
26
|
+
Pass 2: Calculate cosine distance using HNSW indexes on the filtered subset.
|
|
27
|
+
"""
|
|
28
|
+
return (
|
|
29
|
+
self.chunk_model.objects
|
|
30
|
+
# Spatial Pre-Filter: Only keep chunks within the target radius
|
|
31
|
+
.filter(geometry__distance_lte=(target_point, Distance(km=radius_km)))
|
|
32
|
+
# Semantic Search: Calculate cosine distance between chunk and query
|
|
33
|
+
.annotate(semantic_distance=CosineDistance('embedding', query_embedding))
|
|
34
|
+
# Sort by the most semantically relevant
|
|
35
|
+
.order_by('semantic_distance')[:top_k]
|
|
36
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: georag
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Spatial-Semantic Retrieval Augmented Generation framework
|
|
5
|
+
Author-email: Developer <makindeadedeji500@gmail.com>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: django>=4.2
|
|
9
|
+
Requires-Dist: scikit-learn>=1.3.0
|
|
10
|
+
Requires-Dist: psycopg2-binary>=2.9
|
|
11
|
+
Requires-Dist: numpy>=1.24
|
|
12
|
+
|
|
13
|
+
# georag
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/georag/__init__.py
|
|
4
|
+
src/georag/cluster.py
|
|
5
|
+
src/georag/models.py
|
|
6
|
+
src/georag/parsers.py
|
|
7
|
+
src/georag/retrievers.py
|
|
8
|
+
src/georag.egg-info/PKG-INFO
|
|
9
|
+
src/georag.egg-info/SOURCES.txt
|
|
10
|
+
src/georag.egg-info/dependency_links.txt
|
|
11
|
+
src/georag.egg-info/requires.txt
|
|
12
|
+
src/georag.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
georag
|