changes
This commit is contained in:
parent
524580eb25
commit
ffe080adef
82 changed files with 2652 additions and 2956 deletions
|
|
@ -1,118 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
use crate::consts::{
|
||||
AREA_SUMMARY_MAX_TOKENS, AREA_SUMMARY_SYSTEM_PROMPT, AREA_SUMMARY_TEMPERATURE,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::utils::{extract_openai_content, ollama_chat, strip_think_blocks};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NumericStat {
|
||||
name: String,
|
||||
mean: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EnumStat {
|
||||
name: String,
|
||||
counts: std::collections::HashMap<String, u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AreaSummaryRequest {
|
||||
count: usize,
|
||||
location: String,
|
||||
is_postcode: bool,
|
||||
#[serde(default)]
|
||||
filters: Vec<String>,
|
||||
#[serde(default)]
|
||||
numeric_stats: Vec<NumericStat>,
|
||||
#[serde(default)]
|
||||
enum_stats: Vec<EnumStat>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AreaSummaryResponse {
|
||||
summary: String,
|
||||
}
|
||||
|
||||
fn build_prompt(req: &AreaSummaryRequest) -> String {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
let area_type = if req.is_postcode { "postcode" } else { "area" };
|
||||
parts.push(format!(
|
||||
"Summarise this {} of England which contains {} properties matching my requirements.\n",
|
||||
area_type, req.count
|
||||
));
|
||||
|
||||
if !req.filters.is_empty() {
|
||||
parts.push(format!("Active filters: {}.\n", req.filters.join(", ")));
|
||||
}
|
||||
|
||||
if !req.numeric_stats.is_empty() {
|
||||
let stats: Vec<String> = req
|
||||
.numeric_stats
|
||||
.iter()
|
||||
.map(|stat| format!("{}: {:.1}", stat.name, stat.mean))
|
||||
.collect();
|
||||
parts.push(format!(
|
||||
"Average values of the {}: {}.",
|
||||
if req.is_postcode { "postcode" } else { "area" },
|
||||
stats.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
for es in &req.enum_stats {
|
||||
let total: u64 = es.counts.values().sum();
|
||||
if total == 0 {
|
||||
continue;
|
||||
}
|
||||
let mut sorted: Vec<_> = es.counts.iter().collect();
|
||||
sorted.sort_by(|lhs, rhs| rhs.1.cmp(lhs.1));
|
||||
let top: Vec<String> = sorted
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|(val, count)| {
|
||||
let pct = **count as f64 / total as f64 * 100.0;
|
||||
format!("{} ({:.0}%)", val, pct)
|
||||
})
|
||||
.collect();
|
||||
parts.push(format!("{}: {}.", es.name, top.join(", ")));
|
||||
}
|
||||
|
||||
let result = parts.join(" ");
|
||||
info!(prompt = %result, "Built prompt for area summary");
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn post_area_summary(
|
||||
state: Arc<AppState>,
|
||||
Json(req): Json<AreaSummaryRequest>,
|
||||
) -> Result<Json<AreaSummaryResponse>, (StatusCode, String)> {
|
||||
let prompt = build_prompt(&req);
|
||||
info!(location = %req.location, count = req.count, "POST /api/area-summary");
|
||||
|
||||
let url = format!("{}/v1/chat/completions", state.ollama_url);
|
||||
let body = serde_json::json!({
|
||||
"model": state.ollama_model,
|
||||
"messages": [
|
||||
{ "role": "system", "content": AREA_SUMMARY_SYSTEM_PROMPT },
|
||||
{ "role": "user", "content": prompt }
|
||||
],
|
||||
"stream": false,
|
||||
"temperature": AREA_SUMMARY_TEMPERATURE,
|
||||
"max_tokens": AREA_SUMMARY_MAX_TOKENS,
|
||||
});
|
||||
|
||||
let json = ollama_chat(&state.http_client, &url, &body).await?;
|
||||
let content = extract_openai_content(&json)?;
|
||||
|
||||
let summary = strip_think_blocks(content).trim().to_string();
|
||||
|
||||
Ok(Json(AreaSummaryResponse { summary }))
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ use tracing::info;
|
|||
|
||||
use crate::aggregation::Aggregator;
|
||||
use crate::auth::OptionalUser;
|
||||
use crate::consts::MAX_CELLS_PER_REQUEST;
|
||||
use crate::consts::{DEMO_CENTER, DEMO_CENTER_TOLERANCE, MAX_CELLS_PER_REQUEST};
|
||||
use crate::data::travel_time::TravelData;
|
||||
use crate::licensing::check_license_bounds;
|
||||
use crate::parsing::{
|
||||
|
|
@ -190,9 +190,14 @@ pub async fn get_hexagons(
|
|||
let (south, west, north, east) =
|
||||
require_bounds(params.bounds).map_err(IntoResponse::into_response)?;
|
||||
|
||||
// Skip license check at low resolutions (≤5) — data is too aggregated to be
|
||||
// commercially useful, and the homepage demo needs country-wide access.
|
||||
if resolution > 5 {
|
||||
// Allow the homepage demo: check if the center of the requested bounds
|
||||
// is near the demo view center (52.2, -1.9).
|
||||
let center_lat = (south + north) / 2.0;
|
||||
let center_lng = (west + east) / 2.0;
|
||||
let is_demo_view = (center_lat - DEMO_CENTER.0).abs() <= DEMO_CENTER_TOLERANCE
|
||||
&& (center_lng - DEMO_CENTER.1).abs() <= DEMO_CENTER_TOLERANCE;
|
||||
|
||||
if !is_demo_view {
|
||||
check_license_bounds(&user.0, (south, west, north, east))
|
||||
.map_err(|(_, resp)| resp)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ pub struct Property {
|
|||
pub duration: Option<String>,
|
||||
pub current_energy_rating: Option<String>,
|
||||
pub potential_energy_rating: Option<String>,
|
||||
pub listing_status: Option<String>,
|
||||
pub listing_url: Option<String>,
|
||||
pub property_sub_type: Option<String>,
|
||||
pub price_qualifier: Option<String>,
|
||||
|
||||
// Numeric fields
|
||||
pub lat: f32,
|
||||
|
|
@ -48,6 +52,9 @@ pub struct Property {
|
|||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub renovation_history: Vec<RenovationEvent>,
|
||||
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub listing_features: Vec<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub features: FxHashMap<String, f32>,
|
||||
}
|
||||
|
|
@ -231,6 +238,18 @@ pub async fn get_hexagon_properties(
|
|||
lat: state.data.lat[row],
|
||||
lon: state.data.lon[row],
|
||||
renovation_history: state.data.renovation_history(row).to_vec(),
|
||||
listing_features: state.data.listing_features(row).to_vec(),
|
||||
listing_status: lookup_enum_value(
|
||||
feature_name_to_index,
|
||||
feature_data,
|
||||
num_features,
|
||||
enum_values,
|
||||
row,
|
||||
"Listing status",
|
||||
),
|
||||
listing_url: state.data.listing_url(row).map(String::from),
|
||||
property_sub_type: state.data.property_sub_type(row).map(String::from),
|
||||
price_qualifier: state.data.price_qualifier(row).map(String::from),
|
||||
features,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::http::{header, StatusCode, Uri};
|
||||
use axum::http::{header, HeaderMap, StatusCode, Uri};
|
||||
use axum::response::IntoResponse;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
pub async fn get_screenshot(state: Arc<AppState>, uri: Uri) -> impl IntoResponse {
|
||||
pub async fn get_screenshot(
|
||||
state: Arc<AppState>,
|
||||
headers: HeaderMap,
|
||||
uri: Uri,
|
||||
) -> impl IntoResponse {
|
||||
let screenshot_base = &state.screenshot_url;
|
||||
|
||||
let qs = uri
|
||||
|
|
@ -16,7 +20,12 @@ pub async fn get_screenshot(state: Arc<AppState>, uri: Uri) -> impl IntoResponse
|
|||
let url = format!("{screenshot_base}/screenshot{qs}");
|
||||
info!("Proxying screenshot request to: {}", url);
|
||||
|
||||
match state.http_client.get(&url).send().await {
|
||||
let mut req = state.http_client.get(&url);
|
||||
if let Some(auth) = headers.get(header::AUTHORIZATION) {
|
||||
req = req.header(header::AUTHORIZATION, auth);
|
||||
}
|
||||
|
||||
match req.send().await {
|
||||
Ok(resp) if resp.status().is_success() => match resp.bytes().await {
|
||||
Ok(bytes) => (
|
||||
StatusCode::OK,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use rand::Rng;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::pocketbase::auth_superuser;
|
||||
use crate::state::AppState;
|
||||
|
||||
const CODE_LEN: usize = 8;
|
||||
|
|
@ -39,6 +40,22 @@ struct PbRecord {
|
|||
|
||||
pub async fn post_shorten(state: Arc<AppState>, Json(req): Json<ShortenRequest>) -> Response {
|
||||
let pb_url = state.pocketbase_url.trim_end_matches('/');
|
||||
|
||||
let token = match auth_superuser(
|
||||
&state.http_client,
|
||||
pb_url,
|
||||
&state.pocketbase_admin_email,
|
||||
&state.pocketbase_admin_password,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
warn!("PocketBase superuser auth failed: {err}");
|
||||
return StatusCode::BAD_GATEWAY.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let code = generate_code();
|
||||
|
||||
let record = PbRecord {
|
||||
|
|
@ -51,6 +68,7 @@ pub async fn post_shorten(state: Arc<AppState>, Json(req): Json<ShortenRequest>)
|
|||
.post(format!(
|
||||
"{pb_url}/api/collections/short_urls/records"
|
||||
))
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.json(&record)
|
||||
.send()
|
||||
.await;
|
||||
|
|
@ -79,13 +97,33 @@ pub async fn post_shorten(state: Arc<AppState>, Json(req): Json<ShortenRequest>)
|
|||
pub async fn get_short_url(state: Arc<AppState>, Path(code): Path<String>) -> Response {
|
||||
let pb_url = state.pocketbase_url.trim_end_matches('/');
|
||||
|
||||
let token = match auth_superuser(
|
||||
&state.http_client,
|
||||
pb_url,
|
||||
&state.pocketbase_admin_email,
|
||||
&state.pocketbase_admin_password,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
warn!("PocketBase superuser auth failed: {err}");
|
||||
return StatusCode::BAD_GATEWAY.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let filter = format!("code=\"{code}\"");
|
||||
let url = format!(
|
||||
"{pb_url}/api/collections/short_urls/records?filter={}&perPage=1",
|
||||
urlencoding::encode(&filter)
|
||||
);
|
||||
|
||||
let res = state.http_client.get(&url).send().await;
|
||||
let res = state
|
||||
.http_client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue