Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ jest-temp/
# Cache
.cache/

# --- Java 17 / Maven ---
target/
*.class
*.jar
*.war
*.ear
hs_err_pid*

# --- Python / Jupyter ---
__pycache__/
*.pyc
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@ Building civic tools for low-resource communities requires more than technical s
Java model classes aligned with a defined schema
Startup validation logging

# Building the Backend

The backend targets **Java 17**. The build uses a Maven toolchain to compile
and test on JDK 17 regardless of the JVM Maven itself runs under (e.g.
Homebrew's Maven bundles a newer JDK). This requires a one-time, machine-local
`~/.m2/toolchains.xml` registering a JDK 17 install, for example:

```xml
<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0">
<toolchain>
<type>jdk</type>
<provides><version>17</version></provides>
<configuration>
<jdkHome>/path/to/your/jdk-17</jdkHome>
</configuration>
</toolchain>
</toolchains>
```

Find your JDK 17 path with `/usr/libexec/java_home -v 17` (macOS). Without a
matching toolchain entry, the build fails with "No toolchain found".

# Frontend
Lightweight static frontend
Mobile-first interface
Expand Down
Empty file.
22 changes: 22 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@
<workingDirectory>${maven.multiModuleProjectDirectory}</workingDirectory>
</configuration>
</plugin>

<!-- Enforce JDK 17 for compile/test regardless of the JVM Maven runs under.
Requires a matching <jdk> toolchain in ~/.m2/toolchains.xml. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-toolchains-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<toolchains>
<jdk>
<version>17</version>
</jdk>
</toolchains>
</configuration>
<executions>
<execution>
<goals>
<goal>toolchain</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import org.firststep.backend.model.Resource;
import org.firststep.backend.service.ResourceService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -20,6 +21,9 @@ public class ResourceController {

private final ResourceService service;

@Value("${app.seasonal.images.dir:backend/src/main/resources/static/images/seasonal}")
private String seasonalImagesDir;

public ResourceController(ResourceService service) {
this.service = service;
}
Expand All @@ -43,7 +47,7 @@ public ResponseEntity<Resource> getById(@PathVariable String id) {

@GetMapping("/seasonal-images")
public ResponseEntity<List<String>> getSeasonalImages() {
File dir = new File("backend/src/main/resources/static/images/seasonal");
File dir = new File(seasonalImagesDir);
if (!dir.exists() || !dir.isDirectory()) {
return ResponseEntity.ok(Collections.emptyList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.firststep.backend.dto.Citation;
import org.firststep.backend.dto.DecisionRequest;
import org.firststep.backend.dto.DecisionResponse;
import org.firststep.backend.dto.DecisionStep;
import org.firststep.backend.model.NewsItem;
import org.firststep.backend.model.Resource;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -62,8 +58,6 @@ public DecisionResponse decide(DecisionRequest request) {
return resp;
}



List<Resource> resources = resourceService.getAllResources();
List<NewsItem> news = newsService.getAllNews();

Expand Down Expand Up @@ -126,7 +120,7 @@ private List<Resource> selectTopResources(List<Resource> all, String q, boolean

scored.sort(Comparator.comparingInt(ResourceScore::score).reversed());

int limit = 10;
int limit = 5;
return scored.stream().limit(limit).map(ResourceScore::resource).toList();
}

Expand Down Expand Up @@ -156,7 +150,7 @@ private List<NewsItem> selectTopNews(List<NewsItem> all, String q, List<String>

scored.sort(Comparator.comparingInt(NewsScore::score).reversed());

int limit = 6;
int limit = 3;
return scored.stream().limit(limit).map(NewsScore::news).toList();
}

Expand Down Expand Up @@ -188,33 +182,6 @@ private String safeLower(String s) {
return s == null ? "" : s.toLowerCase(Locale.ROOT).trim();
}

private String formatFirstLocation(Resource.Location loc) {
if (loc == null) return null;
if (Boolean.TRUE.equals(loc.confidential)) return null;

String address = loc.address;
String city = loc.city;
String state = loc.state;
String zip = loc.zip;

StringBuilder sb = new StringBuilder();
if (address != null && !address.isBlank()) sb.append(address);

if (city != null && !city.isBlank()) {
if (sb.length() > 0) sb.append(", ");
sb.append(city);
}
if (state != null && !state.isBlank()) {
if (sb.length() > 0) sb.append(", ");
sb.append(state);
}
if (zip != null && !zip.isBlank()) {
sb.append(" ").append(zip);
}

return sb.length() == 0 ? null : sb.toString().trim();
}

private String buildPrompt(
String q,
boolean urgent,
Expand Down Expand Up @@ -256,15 +223,7 @@ private String mapperToTrimmedResourcesJson(List<Resource> resources) {
m.put("category", r.category);
m.put("urgency", r.urgency);
m.put("summary", r.summary);
m.put("description", r.description);
m.put("eligibility", r.eligibility);
m.put("eligibilityAgeMin", r.eligibilityAgeMin);
m.put("eligibilityAgeMax", r.eligibilityAgeMax);
m.put("eligibilityGender", r.eligibilityGender);
m.put("tags", r.tags);
m.put("location", (r.locations != null && !r.locations.isEmpty()) ? formatFirstLocation(r.locations.get(0)) : null);
m.put("phone", (r.phones != null && !r.phones.isEmpty()) ? r.phones.get(0).number : null);
m.put("website", (r.websites != null && !r.websites.isEmpty()) ? r.websites.get(0).url : null);
return m;
}).toList();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package org.firststep.backend.service;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

Expand All @@ -19,14 +18,8 @@ public class NewsService implements DecisionAgentService.NewsServiceLike {

private final ObjectMapper mapper = new ObjectMapper();

private final RssFeedSource rssFeedService;

private List<NewsItem> staticItems = Collections.emptyList();

public NewsService(RssFeedSource rssFeedService) {
this.rssFeedService = rssFeedService;
}

@EventListener(ApplicationReadyEvent.class)
public void init() {
try {
Expand All @@ -47,10 +40,7 @@ public List<NewsItem> getAllNews() {
}

public List<NewsItem> getAll() {
List<NewsItem> merged = new ArrayList<>();
merged.addAll(rssFeedService.getRssItems());
merged.addAll(staticItems);
return Collections.unmodifiableList(merged);
return Collections.unmodifiableList(staticItems);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public String generate(String prompt, double temperature) throws IOException, In
"model", model,
"prompt", prompt,
"stream", false,
"temperature", temperature
"temperature", temperature,
"options", Map.of("num_predict", 1000)
);

String json = mapper.writeValueAsString(body);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.nio.file.Path;

import org.firststep.backend.model.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
Expand All @@ -23,6 +24,9 @@ public class ResourceService implements DecisionAgentService.ResourceServiceLike
private final ObjectMapper mapper = new ObjectMapper();
private List<Resource> resources = Collections.emptyList();

@Value("${app.data.dir:app/data}")
private String dataDir;

/**
* Loads resources after the Spring application is ready.
* Tries external canonical file app/data/resources.json first,
Expand All @@ -31,16 +35,16 @@ public class ResourceService implements DecisionAgentService.ResourceServiceLike
@EventListener(ApplicationReadyEvent.class)
public void init() {
// Try external canonical file first
Path external = Path.of(dataDir, "resources.json");
try {
Path external = Path.of("app", "data", "resources.json");
if (external.toFile().exists()) {
JsonNode root = mapper.readTree(external.toFile());
resources = parseJsonNodeToList(root);
System.out.println("Loaded resources from app/data/resources.json (" + resources.size() + " records)");
System.out.println("Loaded resources from " + external + " (" + resources.size() + " records)");
return;
}
} catch (Exception e) {
System.err.println("Failed to load app/data/resources.json: " + e.getMessage());
System.err.println("Failed to load " + external + ": " + e.getMessage());
}

// Fallback: classpath resources.json
Expand Down
Loading