Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data/Iiams.rmtree filter=lfs diff=lfs merge=lfs -text
6 changes: 4 additions & 2 deletions .github/workflows/pr-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
lfs: true

- name: Install uv
uses: astral-sh/setup-uv@v3
Expand All @@ -29,10 +31,10 @@ jobs:
uv run black --check .

- name: Run tests with coverage
run: uv run pytest --cov=rmagent --cov-report=term-missing --cov-fail-under=80
run: uv run pytest --cov=rmagent --cov-report=term-missing --cov-fail-under=65
env:
# Set test environment variables
RM_DATABASE_PATH: data/test.rmtree
RM_DATABASE_PATH: data/Iiams.rmtree
DEFAULT_LLM_PROVIDER: anthropic
LOG_LEVEL: WARNING

Expand Down
3 changes: 3 additions & 0 deletions data/Iiams.rmtree
Git LFS file not shown
17 changes: 4 additions & 13 deletions rmagent/agent/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from __future__ import annotations

from rmagent.rmlib.parsers.date_parser import parse_rm_date
from rmagent.rmlib.queries import QueryService


class GenealogyFormatters:
Expand Down Expand Up @@ -74,7 +73,7 @@ def format_events(events, event_citations: dict[int, list[int]] | None = None) -
# Add note if present (often contains full article transcriptions)
if note:
# Show "NOTE: " prefix only once, then indent subsequent lines
note_lines = note.split('\n')
note_lines = note.split("\n")
for idx, note_line in enumerate(note_lines):
if note_line.strip():
if idx == 0:
Expand Down Expand Up @@ -233,9 +232,7 @@ def format_siblings(siblings) -> list[str]:
return lines

@staticmethod
def format_early_life(
person, parents, siblings, life_span: dict[str, int | None]
) -> str:
def format_early_life(person, parents, siblings, life_span: dict[str, int | None]) -> str:
"""Format early life narrative with birth order, parental ages, migration notes."""
person_name = GenealogyFormatters.format_person_name(person)
birth_year = life_span.get("birth_year")
Expand Down Expand Up @@ -322,16 +319,10 @@ def format_family_losses(life_span, parents, spouses, siblings, children) -> str
name = GenealogyFormatters.format_person_name(data)
losses.append(f"- {name} ({relation}) died in {death_year_value}.")

return (
"\n".join(losses)
if losses
else "No recorded family deaths occurred during the subject's lifetime."
)
return "\n".join(losses) if losses else "No recorded family deaths occurred during the subject's lifetime."

@staticmethod
def calculate_parent_age(
parents, birth_year_key: str, child_birth_year: int | None
) -> int | None:
def calculate_parent_age(parents, birth_year_key: str, child_birth_year: int | None) -> int | None:
"""Calculate parent's age at child's birth."""
if not parents or child_birth_year is None:
return None
Expand Down
28 changes: 7 additions & 21 deletions rmagent/agent/genealogy_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ class GenealogyAgent:

# ---- Public API -----------------------------------------------------

def generate_biography(
self, person_id: int, style: str = "standard", max_tokens: int | None = None
) -> LLMResult:
def generate_biography(self, person_id: int, style: str = "standard", max_tokens: int | None = None) -> LLMResult:
"""Generate a narrative biography using the configured prompts/LLM."""

context = self._build_biography_context(person_id, style)
Expand All @@ -84,9 +82,7 @@ def _run_validator(db: RMDatabase | None) -> QualityReport:

return self._with_database(_run_validator)

def ask(
self, question: str, person_id: int | None = None, max_tokens: int | None = None
) -> LLMResult:
def ask(self, question: str, person_id: int | None = None, max_tokens: int | None = None) -> LLMResult:
"""Answer ad-hoc questions with light context and persistent memory."""

context = self._build_qa_context(question, person_id)
Expand Down Expand Up @@ -138,15 +134,11 @@ def _builder(db: RMDatabase | None) -> dict[str, str]:
life_span, parents, spouses, siblings, children
)
sibling_lines = GenealogyFormatters.format_siblings(siblings)
sibling_summary = (
"\n".join(sibling_lines) if sibling_lines else "No sibling records available."
)
sibling_summary = "\n".join(sibling_lines) if sibling_lines else "No sibling records available."

# Extract person-level notes
person_notes = person.get("Note") or ""
person_notes_formatted = (
person_notes if person_notes else "No person-level notes available."
)
person_notes_formatted = person_notes if person_notes else "No person-level notes available."

# Generate style-specific length guidance
length_guidance = self._get_length_guidance_for_style(style)
Expand Down Expand Up @@ -185,9 +177,7 @@ def _builder(db: RMDatabase | None) -> dict[str, str]:
snippets.append(GenealogyFormatters.format_family_overview(spouses, children, siblings))
snippets.append(GenealogyFormatters.format_early_life(person, parents, siblings, life_span))

history_snippets = [
f"Q: {turn.question}\nA: {turn.answer}" for turn in self._memory[-3:]
]
history_snippets = [f"Q: {turn.question}\nA: {turn.answer}" for turn in self._memory[-3:]]
snippets.extend(history_snippets)

return {
Expand Down Expand Up @@ -297,9 +287,7 @@ def _fetch_siblings(self, query: QueryService, parents: dict[str, str] | None, p
)
return siblings

def _build_event_citations_map(
self, query: QueryService, events: list[dict]
) -> dict[int, list[int]]:
def _build_event_citations_map(self, query: QueryService, events: list[dict]) -> dict[int, list[int]]:
"""
Build mapping of EventID -> list of CitationIDs for inline citation markers.

Expand Down Expand Up @@ -333,9 +321,7 @@ def _build_event_citations_map(

return event_citations_map

def _collect_all_citations_for_person(
self, query: QueryService, person_id: int
) -> list[dict]:
def _collect_all_citations_for_person(self, query: QueryService, person_id: int) -> list[dict]:
"""
Collect all citations for a person's events using QueryService.
Returns list of citation dicts with CitationID, SourceID, SourceName, CitationName, EventType.
Expand Down
8 changes: 2 additions & 6 deletions rmagent/agent/llm_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,7 @@ def __init__(
self.model = model
self.default_max_tokens = default_max_tokens
self.retry_config = retry_config or RetryConfig()
self.prompt_cost_per_1k, self.completion_cost_per_1k = (
pricing_per_1k if pricing_per_1k else (0.0, 0.0)
)
self.prompt_cost_per_1k, self.completion_cost_per_1k = pricing_per_1k if pricing_per_1k else (0.0, 0.0)

def generate(self, prompt: str, **kwargs: Any) -> LLMResult:
"""Invoke provider with retry semantics."""
Expand Down Expand Up @@ -135,9 +133,7 @@ def _with_cost(self, result: LLMResult) -> LLMResult:
def _invoke(self, prompt: str, **kwargs: Any) -> LLMResult:
"""Concrete providers implement this call."""

def _log_debug(
self, prompt: str, result: LLMResult, elapsed: float, kwargs: dict[str, Any]
) -> None:
def _log_debug(self, prompt: str, result: LLMResult, elapsed: float, kwargs: dict[str, Any]) -> None:
debug_logger = logging.getLogger("rmagent.llm_debug")
if not debug_logger.isEnabledFor(logging.DEBUG):
return
Expand Down
18 changes: 4 additions & 14 deletions rmagent/agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,7 @@ def __init__(self, query_service: QueryService):
self.query_service = query_service

def run(self, person_id: int, generations: int = 3):
return [
dict(row)
for row in self.query_service.get_direct_ancestors(person_id, generations=generations)
]
return [dict(row) for row in self.query_service.get_direct_ancestors(person_id, generations=generations)]


@dataclass
Expand All @@ -99,14 +96,8 @@ def run(self, person_a: int, person_b: int) -> dict[str, str | None]:
if person_a == person_b:
return {"relationship": "Same person"}

ancestors_a = {
row["PersonID"]: row
for row in self.query_service.get_direct_ancestors(person_a, generations=5)
}
ancestors_b = {
row["PersonID"]: row
for row in self.query_service.get_direct_ancestors(person_b, generations=5)
}
ancestors_a = {row["PersonID"]: row for row in self.query_service.get_direct_ancestors(person_a, generations=5)}
ancestors_b = {row["PersonID"]: row for row in self.query_service.get_direct_ancestors(person_b, generations=5)}

shared = set(ancestors_a).intersection(ancestors_b)
if not shared:
Expand Down Expand Up @@ -137,8 +128,7 @@ def run(self):
report = validator.run_all_checks()
return {
"totals_by_severity": {
k.value if hasattr(k, "value") else str(k): v
for k, v in report.totals_by_severity.items()
k.value if hasattr(k, "value") else str(k): v for k, v in report.totals_by_severity.items()
},
"totals_by_category": report.totals_by_category,
"issue_count": report.summary.get("issue_total", 0),
Expand Down
3 changes: 2 additions & 1 deletion rmagent/cli/commands/bio.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ def bio(
}[citation_style.lower()]

# Create generator and agent
config = ctx.load_config()
# Skip LLM credential validation if using template-based generation
config = ctx.load_config(require_llm_credentials=not no_ai)
agent = (
None
if no_ai
Expand Down
6 changes: 2 additions & 4 deletions rmagent/cli/commands/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def hugo(
}[bio_length.lower()]

# Create exporter
config = ctx.load_config()
config = ctx.load_config(require_llm_credentials=False)
exporter = HugoExporter(
db=config.database.database_path,
extension_path=config.database.sqlite_extension_path,
Expand All @@ -100,9 +100,7 @@ def hugo(
# Get all person IDs
from rmagent.rmlib.database import RMDatabase

with RMDatabase(
config.database.database_path, extension_path=config.database.sqlite_extension_path
) as db:
with RMDatabase(config.database.database_path, extension_path=config.database.sqlite_extension_path) as db:
all_persons = db.query("SELECT PersonID FROM PersonTable")
person_ids = [p["PersonID"] for p in all_persons]

Expand Down
25 changes: 7 additions & 18 deletions rmagent/cli/commands/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ def person(ctx, person_id: int, events: bool, ancestors: bool, descendants: bool
raise click.Abort()

# Display person header
name = (
f"{_get_value(person_data, 'Given')} {_get_value(person_data, 'Surname')}".strip()
)
name = f"{_get_value(person_data, 'Given')} {_get_value(person_data, 'Surname')}".strip()
birth_year = _get_value(person_data, "BirthYear", "?")
death_year = _get_value(person_data, "DeathYear", "?")
console.print(f"\n[bold]📋 Person: {name}[/bold] ({birth_year}–{death_year})")
Expand All @@ -68,9 +66,7 @@ def person(ctx, person_id: int, events: bool, ancestors: bool, descendants: bool
from rmagent.rmlib.parsers.date_parser import parse_rm_date

date_str = _get_value(event, "Date")
formatted_date = (
parse_rm_date(date_str).format_display() if date_str else ""
)
formatted_date = parse_rm_date(date_str).format_display() if date_str else ""
table.add_row(
formatted_date,
_get_value(event, "EventType"),
Expand All @@ -89,15 +85,13 @@ def person(ctx, person_id: int, events: bool, ancestors: bool, descendants: bool
# Check for father
if _get_value(parents_row, "FatherID"):
father_name = (
f"{_get_value(parents_row, 'FatherGiven')} "
f"{_get_value(parents_row, 'FatherSurname')}"
f"{_get_value(parents_row, 'FatherGiven')} " f"{_get_value(parents_row, 'FatherSurname')}"
).strip()
console.print(f" • Father: {father_name}")
# Check for mother
if _get_value(parents_row, "MotherID"):
mother_name = (
f"{_get_value(parents_row, 'MotherGiven')} "
f"{_get_value(parents_row, 'MotherSurname')}"
f"{_get_value(parents_row, 'MotherGiven')} " f"{_get_value(parents_row, 'MotherSurname')}"
).strip()
console.print(f" • Mother: {mother_name}")

Expand All @@ -106,19 +100,15 @@ def person(ctx, person_id: int, events: bool, ancestors: bool, descendants: bool
if spouses:
console.print("\n[bold]Spouses:[/bold]")
for spouse in spouses:
spouse_name = (
f"{_get_value(spouse, 'Given')} {_get_value(spouse, 'Surname')}".strip()
)
spouse_name = f"{_get_value(spouse, 'Given')} {_get_value(spouse, 'Surname')}".strip()
console.print(f" • {spouse_name}")

# Get children
children = queries.get_children(person_id)
if children:
console.print("\n[bold]Children:[/bold]")
for child in children:
child_name = (
f"{_get_value(child, 'Given')} {_get_value(child, 'Surname')}".strip()
)
child_name = f"{_get_value(child, 'Given')} {_get_value(child, 'Surname')}".strip()
console.print(f" • {child_name}")

# Show ancestors if requested
Expand All @@ -141,8 +131,7 @@ def person(ctx, person_id: int, events: bool, ancestors: bool, descendants: bool
console.print("\n[bold]Descendants:[/bold] (4 generations)")
for descendant in descendant_rows:
descendant_name = (
f"{_get_value(descendant, 'Given')} "
f"{_get_value(descendant, 'Surname')}"
f"{_get_value(descendant, 'Given')} " f"{_get_value(descendant, 'Surname')}"
).strip()
gen = _get_value(descendant, "Generation", 1)
indent = " " * gen
Expand Down
6 changes: 2 additions & 4 deletions rmagent/cli/commands/quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def quality(
task = progress.add_task("Running data quality validation...", total=None)

# Create generator
config = ctx.load_config()
config = ctx.load_config(require_llm_credentials=False)
generator = QualityReportGenerator(
db=config.database.database_path,
extension_path=config.database.sqlite_extension_path,
Expand Down Expand Up @@ -141,9 +141,7 @@ def quality(
console.print()
console.print(report_output)
else:
console.print(
"[yellow]Warning:[/yellow] HTML and CSV formats require --output option"
)
console.print("[yellow]Warning:[/yellow] HTML and CSV formats require --output option")

except Exception as e:
console.print(f"\n[red]Error:[/red] {e}")
Expand Down
Loading
Loading