us-cpa: OpenClaw skill wrapper for U.S. federal individual tax work #1
@@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
`us-cpa` is a Python CLI plus OpenClaw skill wrapper for U.S. federal individual tax work.
|
`us-cpa` is a Python CLI plus OpenClaw skill wrapper for U.S. federal individual tax work.
|
||||||
|
|
||||||
|
## Standalone package usage
|
||||||
|
|
||||||
|
From `skills/us-cpa/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .[dev]
|
||||||
|
us-cpa --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Without installing, the repo-local wrapper works directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skills/us-cpa/scripts/us-cpa --help
|
||||||
|
```
|
||||||
|
|
||||||
## Current Milestone
|
## Current Milestone
|
||||||
|
|
||||||
Current implementation now includes:
|
Current implementation now includes:
|
||||||
@@ -13,10 +28,9 @@ Current implementation now includes:
|
|||||||
- case-folder intake and conflict-stop handling
|
- case-folder intake and conflict-stop handling
|
||||||
- question workflow with conversation and memo output
|
- question workflow with conversation and memo output
|
||||||
- prepare workflow for the current supported 1040 subset
|
- prepare workflow for the current supported 1040 subset
|
||||||
|
- review workflow with findings-first output
|
||||||
- e-file-ready draft export payload generation
|
- e-file-ready draft export payload generation
|
||||||
|
|
||||||
Review logic and broader form coverage are still pending.
|
|
||||||
|
|
||||||
## CLI Surface
|
## CLI Surface
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
45
skills/us-cpa/README.md
Normal file
45
skills/us-cpa/README.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# us-cpa package
|
||||||
|
|
||||||
|
Standalone Python CLI package for the `us-cpa` skill.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
From `skills/us-cpa/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .[dev]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
Installed entry point:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
us-cpa --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Repo-local wrapper without installation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/us-cpa --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Module execution:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m us_cpa.cli --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
From `skills/us-cpa/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src python3 -m unittest
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with the dev extra installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m unittest
|
||||||
|
```
|
||||||
@@ -7,7 +7,15 @@ name = "us-cpa"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "US federal individual tax workflow CLI for questions, preparation, and review."
|
description = "US federal individual tax workflow CLI for questions, preparation, and review."
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"pypdf>=5.0.0",
|
||||||
|
"reportlab>=4.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
us-cpa = "us_cpa.cli:main"
|
us-cpa = "us_cpa.cli:main"
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ TOPIC_RULES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
RISK_BY_CONFIDENCE = {
|
||||||
|
"high": "low",
|
||||||
|
"medium": "medium",
|
||||||
|
"low": "high",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_question(question: str) -> str:
|
def _normalize_question(question: str) -> str:
|
||||||
return question.strip().lower()
|
return question.strip().lower()
|
||||||
|
|
||||||
@@ -98,6 +105,7 @@ class QuestionEngine:
|
|||||||
"authorities": authorities,
|
"authorities": authorities,
|
||||||
"conclusion": {"answer": answer, "summary": summary},
|
"conclusion": {"answer": answer, "summary": summary},
|
||||||
"confidence": rule["confidence"],
|
"confidence": rule["confidence"],
|
||||||
|
"riskLevel": RISK_BY_CONFIDENCE[rule["confidence"]],
|
||||||
"followUpQuestions": [],
|
"followUpQuestions": [],
|
||||||
"primaryLawRequired": False,
|
"primaryLawRequired": False,
|
||||||
}
|
}
|
||||||
@@ -115,6 +123,7 @@ class QuestionEngine:
|
|||||||
"summary": "This question needs primary-law analysis before a reliable answer can be given.",
|
"summary": "This question needs primary-law analysis before a reliable answer can be given.",
|
||||||
},
|
},
|
||||||
"confidence": "low",
|
"confidence": "low",
|
||||||
|
"riskLevel": "high",
|
||||||
"followUpQuestions": [
|
"followUpQuestions": [
|
||||||
"What facts drive the section-level issue?",
|
"What facts drive the section-level issue?",
|
||||||
"Is there an existing return position or drafted treatment to review?",
|
"Is there an existing return position or drafted treatment to review?",
|
||||||
@@ -125,6 +134,9 @@ class QuestionEngine:
|
|||||||
|
|
||||||
def render_analysis(analysis: dict[str, Any]) -> str:
|
def render_analysis(analysis: dict[str, Any]) -> str:
|
||||||
lines = [analysis["conclusion"]["summary"]]
|
lines = [analysis["conclusion"]["summary"]]
|
||||||
|
lines.append(
|
||||||
|
f"Confidence: {analysis['confidence']}. Risk: {analysis['riskLevel']}."
|
||||||
|
)
|
||||||
if analysis["factsUsed"]:
|
if analysis["factsUsed"]:
|
||||||
facts = ", ".join(f"{item['field']}={item['value']}" for item in analysis["factsUsed"])
|
facts = ", ".join(f"{item['field']}={item['value']}" for item in analysis["factsUsed"])
|
||||||
lines.append(f"Facts used: {facts}.")
|
lines.append(f"Facts used: {facts}.")
|
||||||
@@ -160,6 +172,8 @@ def render_memo(analysis: dict[str, Any]) -> str:
|
|||||||
"",
|
"",
|
||||||
"## Analysis",
|
"## Analysis",
|
||||||
analysis["conclusion"]["summary"],
|
analysis["conclusion"]["summary"],
|
||||||
|
f"Confidence: {analysis['confidence']}",
|
||||||
|
f"Risk level: {analysis['riskLevel']}",
|
||||||
"",
|
"",
|
||||||
"## Conclusion",
|
"## Conclusion",
|
||||||
analysis["conclusion"]["answer"],
|
analysis["conclusion"]["answer"],
|
||||||
|
|||||||
@@ -1,45 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from us_cpa.tax_years import tax_year_rules
|
||||||
STANDARD_DEDUCTION_2025 = {
|
|
||||||
"single": 15750.0,
|
|
||||||
"married_filing_jointly": 31500.0,
|
|
||||||
"head_of_household": 23625.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
TAX_BRACKETS_2025 = {
|
|
||||||
"single": [
|
|
||||||
(11925.0, 0.10),
|
|
||||||
(48475.0, 0.12),
|
|
||||||
(103350.0, 0.22),
|
|
||||||
(197300.0, 0.24),
|
|
||||||
(250525.0, 0.32),
|
|
||||||
(626350.0, 0.35),
|
|
||||||
(float("inf"), 0.37),
|
|
||||||
],
|
|
||||||
"married_filing_jointly": [
|
|
||||||
(23850.0, 0.10),
|
|
||||||
(96950.0, 0.12),
|
|
||||||
(206700.0, 0.22),
|
|
||||||
(394600.0, 0.24),
|
|
||||||
(501050.0, 0.32),
|
|
||||||
(751600.0, 0.35),
|
|
||||||
(float("inf"), 0.37),
|
|
||||||
],
|
|
||||||
"head_of_household": [
|
|
||||||
(17000.0, 0.10),
|
|
||||||
(64850.0, 0.12),
|
|
||||||
(103350.0, 0.22),
|
|
||||||
(197300.0, 0.24),
|
|
||||||
(250500.0, 0.32),
|
|
||||||
(626350.0, 0.35),
|
|
||||||
(float("inf"), 0.37),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _as_float(value: Any) -> float:
|
def _as_float(value: Any) -> float:
|
||||||
@@ -48,9 +11,9 @@ def _as_float(value: Any) -> float:
|
|||||||
return float(value)
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
def tax_on_ordinary_income(amount: float, filing_status: str) -> float:
|
def tax_on_ordinary_income(amount: float, filing_status: str, tax_year: int) -> float:
|
||||||
taxable = max(0.0, amount)
|
taxable = max(0.0, amount)
|
||||||
brackets = TAX_BRACKETS_2025[filing_status]
|
brackets = tax_year_rules(tax_year)["ordinaryIncomeBrackets"][filing_status]
|
||||||
lower = 0.0
|
lower = 0.0
|
||||||
tax = 0.0
|
tax = 0.0
|
||||||
for upper, rate in brackets:
|
for upper, rate in brackets:
|
||||||
@@ -72,6 +35,7 @@ def resolve_required_forms(normalized: dict[str, Any]) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def normalize_case_facts(facts: dict[str, Any], tax_year: int) -> dict[str, Any]:
|
def normalize_case_facts(facts: dict[str, Any], tax_year: int) -> dict[str, Any]:
|
||||||
|
rules = tax_year_rules(tax_year)
|
||||||
filing_status = facts.get("filingStatus", "single")
|
filing_status = facts.get("filingStatus", "single")
|
||||||
wages = _as_float(facts.get("wages"))
|
wages = _as_float(facts.get("wages"))
|
||||||
interest = _as_float(facts.get("taxableInterest"))
|
interest = _as_float(facts.get("taxableInterest"))
|
||||||
@@ -79,9 +43,9 @@ def normalize_case_facts(facts: dict[str, Any], tax_year: int) -> dict[str, Any]
|
|||||||
withholding = _as_float(facts.get("federalWithholding"))
|
withholding = _as_float(facts.get("federalWithholding"))
|
||||||
|
|
||||||
adjusted_gross_income = wages + interest + business_income
|
adjusted_gross_income = wages + interest + business_income
|
||||||
standard_deduction = STANDARD_DEDUCTION_2025[filing_status]
|
standard_deduction = rules["standardDeduction"][filing_status]
|
||||||
taxable_income = max(0.0, adjusted_gross_income - standard_deduction)
|
taxable_income = max(0.0, adjusted_gross_income - standard_deduction)
|
||||||
income_tax = tax_on_ordinary_income(taxable_income, filing_status)
|
income_tax = tax_on_ordinary_income(taxable_income, filing_status, tax_year)
|
||||||
self_employment_tax = round(max(0.0, business_income) * 0.9235 * 0.153, 2)
|
self_employment_tax = round(max(0.0, business_income) * 0.9235 * 0.153, 2)
|
||||||
total_tax = round(income_tax + self_employment_tax, 2)
|
total_tax = round(income_tax + self_employment_tax, 2)
|
||||||
total_payments = withholding
|
total_payments = withholding
|
||||||
|
|||||||
93
skills/us-cpa/src/us_cpa/tax_years.py
Normal file
93
skills/us-cpa/src/us_cpa/tax_years.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
TAX_YEAR_DATA: dict[int, dict[str, Any]] = {
|
||||||
|
2024: {
|
||||||
|
"standardDeduction": {
|
||||||
|
"single": 14600.0,
|
||||||
|
"married_filing_jointly": 29200.0,
|
||||||
|
"head_of_household": 21900.0,
|
||||||
|
},
|
||||||
|
"ordinaryIncomeBrackets": {
|
||||||
|
"single": [
|
||||||
|
(11600.0, 0.10),
|
||||||
|
(47150.0, 0.12),
|
||||||
|
(100525.0, 0.22),
|
||||||
|
(191950.0, 0.24),
|
||||||
|
(243725.0, 0.32),
|
||||||
|
(609350.0, 0.35),
|
||||||
|
(float("inf"), 0.37),
|
||||||
|
],
|
||||||
|
"married_filing_jointly": [
|
||||||
|
(23200.0, 0.10),
|
||||||
|
(94300.0, 0.12),
|
||||||
|
(201050.0, 0.22),
|
||||||
|
(383900.0, 0.24),
|
||||||
|
(487450.0, 0.32),
|
||||||
|
(731200.0, 0.35),
|
||||||
|
(float("inf"), 0.37),
|
||||||
|
],
|
||||||
|
"head_of_household": [
|
||||||
|
(16550.0, 0.10),
|
||||||
|
(63100.0, 0.12),
|
||||||
|
(100500.0, 0.22),
|
||||||
|
(191950.0, 0.24),
|
||||||
|
(243700.0, 0.32),
|
||||||
|
(609350.0, 0.35),
|
||||||
|
(float("inf"), 0.37),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
2025: {
|
||||||
|
"standardDeduction": {
|
||||||
|
"single": 15750.0,
|
||||||
|
"married_filing_jointly": 31500.0,
|
||||||
|
"head_of_household": 23625.0,
|
||||||
|
},
|
||||||
|
"ordinaryIncomeBrackets": {
|
||||||
|
"single": [
|
||||||
|
(11925.0, 0.10),
|
||||||
|
(48475.0, 0.12),
|
||||||
|
(103350.0, 0.22),
|
||||||
|
(197300.0, 0.24),
|
||||||
|
(250525.0, 0.32),
|
||||||
|
(626350.0, 0.35),
|
||||||
|
(float("inf"), 0.37),
|
||||||
|
],
|
||||||
|
"married_filing_jointly": [
|
||||||
|
(23850.0, 0.10),
|
||||||
|
(96950.0, 0.12),
|
||||||
|
(206700.0, 0.22),
|
||||||
|
(394600.0, 0.24),
|
||||||
|
(501050.0, 0.32),
|
||||||
|
(751600.0, 0.35),
|
||||||
|
(float("inf"), 0.37),
|
||||||
|
],
|
||||||
|
"head_of_household": [
|
||||||
|
(17000.0, 0.10),
|
||||||
|
(64850.0, 0.12),
|
||||||
|
(103350.0, 0.22),
|
||||||
|
(197300.0, 0.24),
|
||||||
|
(250500.0, 0.32),
|
||||||
|
(626350.0, 0.35),
|
||||||
|
(float("inf"), 0.37),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def supported_tax_years() -> list[int]:
|
||||||
|
return sorted(TAX_YEAR_DATA)
|
||||||
|
|
||||||
|
|
||||||
|
def tax_year_rules(tax_year: int) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return TAX_YEAR_DATA[tax_year]
|
||||||
|
except KeyError as exc:
|
||||||
|
years = ", ".join(str(year) for year in supported_tax_years())
|
||||||
|
raise ValueError(
|
||||||
|
f"Unsupported tax year {tax_year}. Supported tax years: {years}."
|
||||||
|
) from exc
|
||||||
@@ -13,15 +13,33 @@ SKILL_DIR = Path(__file__).resolve().parents[1]
|
|||||||
SRC_DIR = SKILL_DIR / "src"
|
SRC_DIR = SKILL_DIR / "src"
|
||||||
|
|
||||||
|
|
||||||
|
def _pyproject_text() -> str:
|
||||||
|
return (SKILL_DIR / "pyproject.toml").read_text()
|
||||||
|
|
||||||
|
|
||||||
class UsCpaCliSmokeTests(unittest.TestCase):
|
class UsCpaCliSmokeTests(unittest.TestCase):
|
||||||
def test_skill_scaffold_files_exist(self) -> None:
|
def test_skill_scaffold_files_exist(self) -> None:
|
||||||
self.assertTrue((SKILL_DIR / "SKILL.md").exists())
|
self.assertTrue((SKILL_DIR / "SKILL.md").exists())
|
||||||
self.assertTrue((SKILL_DIR / "pyproject.toml").exists())
|
self.assertTrue((SKILL_DIR / "pyproject.toml").exists())
|
||||||
|
self.assertTrue((SKILL_DIR / "README.md").exists())
|
||||||
self.assertTrue((SKILL_DIR / "scripts" / "us-cpa").exists())
|
self.assertTrue((SKILL_DIR / "scripts" / "us-cpa").exists())
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
(SKILL_DIR.parent.parent / "docs" / "us-cpa.md").exists()
|
(SKILL_DIR.parent.parent / "docs" / "us-cpa.md").exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_pyproject_declares_runtime_and_dev_dependencies(self) -> None:
|
||||||
|
pyproject = _pyproject_text()
|
||||||
|
self.assertIn('"pypdf>=', pyproject)
|
||||||
|
self.assertIn('"reportlab>=', pyproject)
|
||||||
|
self.assertIn("[project.optional-dependencies]", pyproject)
|
||||||
|
self.assertIn('"pytest>=', pyproject)
|
||||||
|
|
||||||
|
def test_readme_documents_install_and_script_usage(self) -> None:
|
||||||
|
readme = (SKILL_DIR / "README.md").read_text()
|
||||||
|
self.assertIn("pip install -e .[dev]", readme)
|
||||||
|
self.assertIn("scripts/us-cpa", readme)
|
||||||
|
self.assertIn("python -m unittest", readme)
|
||||||
|
|
||||||
def test_fixture_directories_exist(self) -> None:
|
def test_fixture_directories_exist(self) -> None:
|
||||||
fixtures_dir = SKILL_DIR / "tests" / "fixtures"
|
fixtures_dir = SKILL_DIR / "tests" / "fixtures"
|
||||||
for name in ("irs", "facts", "documents", "returns"):
|
for name in ("irs", "facts", "documents", "returns"):
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class QuestionEngineTests(unittest.TestCase):
|
|||||||
self.assertEqual(analysis["taxYear"], 2025)
|
self.assertEqual(analysis["taxYear"], 2025)
|
||||||
self.assertEqual(analysis["conclusion"]["answer"], "$15,750")
|
self.assertEqual(analysis["conclusion"]["answer"], "$15,750")
|
||||||
self.assertEqual(analysis["confidence"], "high")
|
self.assertEqual(analysis["confidence"], "high")
|
||||||
|
self.assertEqual(analysis["riskLevel"], "low")
|
||||||
self.assertTrue(analysis["authorities"])
|
self.assertTrue(analysis["authorities"])
|
||||||
self.assertEqual(analysis["authorities"][0]["sourceClass"], "irs_instructions")
|
self.assertEqual(analysis["authorities"][0]["sourceClass"], "irs_instructions")
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ class QuestionEngineTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(analysis["confidence"], "low")
|
self.assertEqual(analysis["confidence"], "low")
|
||||||
|
self.assertEqual(analysis["riskLevel"], "high")
|
||||||
self.assertTrue(analysis["primaryLawRequired"])
|
self.assertTrue(analysis["primaryLawRequired"])
|
||||||
self.assertIn("Internal Revenue Code", analysis["missingFacts"][0])
|
self.assertIn("Internal Revenue Code", analysis["missingFacts"][0])
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ class QuestionEngineTests(unittest.TestCase):
|
|||||||
"authorities": [{"title": "Instructions for Form 1040 and Schedules 1-3"}],
|
"authorities": [{"title": "Instructions for Form 1040 and Schedules 1-3"}],
|
||||||
"conclusion": {"answer": "$15,750", "summary": "Single filers use a $15,750 standard deduction for tax year 2025."},
|
"conclusion": {"answer": "$15,750", "summary": "Single filers use a $15,750 standard deduction for tax year 2025."},
|
||||||
"confidence": "high",
|
"confidence": "high",
|
||||||
|
"riskLevel": "low",
|
||||||
"followUpQuestions": [],
|
"followUpQuestions": [],
|
||||||
"primaryLawRequired": False,
|
"primaryLawRequired": False,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ class ReturnModelTests(unittest.TestCase):
|
|||||||
def test_tax_bracket_calculation_uses_2025_single_rates(self) -> None:
|
def test_tax_bracket_calculation_uses_2025_single_rates(self) -> None:
|
||||||
self.assertEqual(tax_on_ordinary_income(34350.0, "single"), 3883.5)
|
self.assertEqual(tax_on_ordinary_income(34350.0, "single"), 3883.5)
|
||||||
|
|
||||||
|
def test_tax_bracket_calculation_uses_selected_tax_year(self) -> None:
|
||||||
|
self.assertEqual(tax_on_ordinary_income(33650.0, "single", 2024), 3806.0)
|
||||||
|
|
||||||
|
def test_normalize_case_facts_rejects_unsupported_tax_year(self) -> None:
|
||||||
|
with self.assertRaisesRegex(ValueError, "Unsupported tax year"):
|
||||||
|
normalize_case_facts({"filingStatus": "single"}, 2023)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user