Skip to content

Commit 5bed493

Browse files
authored
Merge pull request #1 from delschlangen/claude/enhance-bluebook-citations-uO8FO
Enhance Bluebook citation tool with smart research and auto-sourcing
2 parents cee91f7 + e1d1c3d commit 5bed493

File tree

4 files changed

+1073
-58
lines changed

4 files changed

+1073
-58
lines changed

bluebook-citation-generator/backend/app/main.py

Lines changed: 155 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
from .services.bluebook_rules import BluebookFormatter, ShortFormManager
1313
from .services.context_analyzer import DocumentContextAnalyzer
1414
from .services.lookup_service import LegalLookupService, CitationCompleter
15-
from .services.source_finder import ClaimDetector
15+
from .services.source_finder import ClaimDetector, SourceFinder
1616
from .models.citation import (
17-
Citation, DocumentAnalysis, UploadResponse,
18-
AnalysisResponse, AnalysisStats, CitationType
17+
Citation, DocumentAnalysis, UploadResponse,
18+
AnalysisResponse, AnalysisStats, CitationType, CitationStatus
1919
)
2020

2121
# Global services
@@ -199,31 +199,179 @@ async def lookup_case(
199199
"""Look up a case by parties or citation string."""
200200
search_citation = Citation(
201201
type=CitationType.CASE,
202-
status="incomplete",
202+
status=CitationStatus.INCOMPLETE,
203203
raw_text=parties or citation or "",
204204
position_start=0,
205205
position_end=0,
206206
)
207-
207+
208208
if parties and " v. " in parties:
209209
parts = parties.split(" v. ")
210210
search_citation.parties = [p.strip() for p in parts]
211211
elif parties and " v " in parties:
212212
parts = parties.split(" v ")
213213
search_citation.parties = [p.strip() for p in parts]
214-
214+
215215
if citation:
216216
import re
217217
match = re.match(r"(\d+)\s+([A-Za-z.\s]+)\s+(\d+)", citation)
218218
if match:
219219
search_citation.volume = match.group(1)
220220
search_citation.reporter = match.group(2).strip()
221221
search_citation.page = match.group(3)
222-
222+
223223
results = await lookup_service.lookup_citation(search_citation)
224224
return results
225225

226226

227+
@app.post("/api/complete-from-text")
228+
async def complete_from_text(text: str = Body(..., embed=True)):
229+
"""
230+
Complete a citation from minimal text input.
231+
232+
Accepts:
233+
- Case names (e.g., "Roe v. Wade")
234+
- Partial citations (e.g., "Brown v Board")
235+
- URLs (e.g., "https://example.com/article")
236+
- Article titles or author names
237+
- Statute references (e.g., "42 USC 1983")
238+
239+
Returns a completed citation with all available information.
240+
"""
241+
completer = CitationCompleter(lookup_service)
242+
citation, results = await completer.complete_from_text(text)
243+
244+
# Format the completed citation
245+
formatted = formatter.format_citation(citation)
246+
247+
return {
248+
"input": text,
249+
"citation": citation.model_dump(),
250+
"formatted": formatted,
251+
"lookup_results": results,
252+
"confidence": citation.confidence_score,
253+
"status": citation.status.value,
254+
}
255+
256+
257+
@app.post("/api/find-sources")
258+
async def find_sources(
259+
text: str = Body(...),
260+
max_suggestions: int = Body(default=3),
261+
):
262+
"""
263+
Find potential sources for unsourced claims in text.
264+
265+
Analyzes the text for claims that need citations and
266+
searches legal databases for relevant sources.
267+
"""
268+
source_finder = SourceFinder(lookup_service)
269+
270+
# First detect claims
271+
claim_detector = ClaimDetector()
272+
claims = claim_detector.detect_unsourced_claims(text, [])
273+
274+
# Find sources for each claim
275+
enriched_claims = await source_finder.find_sources_for_claims(claims, max_suggestions)
276+
277+
return {
278+
"total_claims": len(claims),
279+
"claims_with_sources": sum(1 for c in enriched_claims if c.get("suggested_sources")),
280+
"claims": enriched_claims,
281+
}
282+
283+
284+
@app.post("/api/analyze-comprehensive")
285+
async def analyze_comprehensive(
286+
document_id: str = Body(...),
287+
text: str = Body(...),
288+
filename: str = Body(default="document"),
289+
find_sources: bool = Body(default=True),
290+
):
291+
"""
292+
Comprehensive document analysis with all features.
293+
294+
Includes:
295+
- Citation extraction and completion
296+
- Short form suggestions
297+
- Unsourced claim detection with source suggestions
298+
- Citation summary statistics
299+
"""
300+
# Extract citations
301+
citations = extractor.extract_all(text)
302+
303+
# Complete incomplete citations using smart lookup
304+
completer = CitationCompleter(lookup_service)
305+
completed_citations = []
306+
307+
for citation in citations:
308+
if citation.status.value in ["incomplete", "needs_verification"]:
309+
citation = await completer.complete_citation(citation)
310+
311+
# Generate formatted suggestion
312+
citation.suggested_correction = formatter.format_citation(citation)
313+
completed_citations.append(citation)
314+
315+
# Analyze citation sequence for short forms
316+
context_analyzer = DocumentContextAnalyzer()
317+
short_form_suggestions = context_analyzer.analyze_citation_sequence(completed_citations)
318+
319+
# Get citation summary
320+
citation_summary = context_analyzer.get_citation_summary(completed_citations)
321+
322+
# Detect and find sources for unsourced claims
323+
unsourced_analysis = None
324+
if find_sources:
325+
source_finder = SourceFinder(lookup_service)
326+
citation_positions = [(c.position_start, c.position_end) for c in completed_citations]
327+
unsourced_analysis = await source_finder.analyze_document_for_sources(
328+
text, citation_positions
329+
)
330+
331+
# Calculate stats
332+
stats = AnalysisStats(
333+
total_citations=len(completed_citations),
334+
complete=sum(1 for c in completed_citations if c.status.value == "complete"),
335+
incomplete=sum(1 for c in completed_citations if c.status.value == "incomplete"),
336+
needs_verification=sum(1 for c in completed_citations if c.status.value == "needs_verification"),
337+
unsourced_claims=unsourced_analysis["total_claims"] if unsourced_analysis else 0,
338+
)
339+
340+
# Build analysis
341+
analysis = DocumentAnalysis(
342+
document_id=document_id,
343+
filename=filename,
344+
total_footnotes=max((c.footnote_number or 0) for c in completed_citations) if completed_citations else 0,
345+
citations=completed_citations,
346+
unsourced_claims=[], # Simplified, full data in unsourced_analysis
347+
)
348+
349+
return {
350+
"analysis": analysis.model_dump(),
351+
"short_form_suggestions": short_form_suggestions,
352+
"stats": stats.model_dump(),
353+
"citation_summary": citation_summary,
354+
"unsourced_analysis": unsourced_analysis,
355+
}
356+
357+
358+
@app.post("/api/search")
359+
async def search_citations(
360+
query: str = Body(...),
361+
search_type: str = Body(default="case"),
362+
):
363+
"""
364+
Search legal databases for citations.
365+
366+
search_type can be:
367+
- "case": Search for case law
368+
- "article": Search for law review articles
369+
- "statute": Parse and lookup statutes
370+
"""
371+
results = await lookup_service.search_by_text(query, search_type)
372+
return results
373+
374+
227375
if __name__ == "__main__":
228376
import uvicorn
229377
uvicorn.run(app, host="0.0.0.0", port=8000)

0 commit comments

Comments
 (0)