Compare commits

...

2 Commits

Author SHA1 Message Date
aki
f02f848ada feat: Implement full problem generation
This commit marks a major milestone in the problem generator project.

Key systems include:
- Fully functional problem generation for all 23 defined concepts (Simple Interest, Compound Interest, Banker's Discount, Effective Rate, Continuous Compounding, Exact/Ordinary Simple Interest).
- Robust date handling for date-specific interest calculations.
- Improved solution presentation with accurate "Substitute Values" step and complete variable descriptions.
- Interactive problem display in `main.py`: problem statement shown first, question and solution revealed on user input.
- Added plausibility checks for rate calculations to avoid unrealistic negative rates.
- Comprehensive `README.md` update:
    - Detailed system architecture (modules, data files, Mermaid diagram).
    - List of all currently covered financial concepts.
    - Instructions for running and extending the generator.
    - Discussion of scope for future enhancements (Equation of Value, Gradients, etc.).
- Refinements to `value_sampler.py` for better formatting and handling of None values.
- Updates to `text_snippets.json` for complete variable descriptions and improved solution step phrasing.
- Updates to `value_ranges.json` for date generation parameters.
- `problem_engine.py` now systematically tests all concepts when run directly.
- Added `uv.lock` to track resolved dependencies.

The system is capable of generating a wide variety of engineering economy problems with detailed, step-by-step solutions and an interactive user experience.
2025-05-09 11:56:16 +08:00
aki
f1d18ec989 feat: Add the Python project 2025-05-09 10:22:35 +08:00
16 changed files with 2693 additions and 1 deletions

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.9

243
README.md
View File

@ -1,2 +1,243 @@
# problem-generator
# Engineering Economy Problem Generator
This project is a Python-based system designed to procedurally generate engineering economy problems. It focuses on topics such as simple and compound interest, effective interest rates, continuous compounding, Banker's Discount, and date-specific interest calculations (exact and ordinary). The system aims to create varied and realistic word problems with step-by-step solutions, suitable for practice and learning.
## Covered Concepts
The generator currently supports problems related to the following topics (aligned with typical Engineering Economy curricula):
* **Simple Interest:**
* Calculating Future Value (F)
* Calculating Present Value (P) from Future Value
* Calculating Simple Interest Amount (I)
* Calculating Present Value (P) from Interest Amount
* Calculating Simple Interest Rate (i)
* Calculating Time Period (n)
* **Types of Simple Interest:**
* Exact Simple Interest (using actual days in month/year, including leap year considerations)
* Ordinary Simple Interest (using 30-day months, 360-day year)
* **Banker's Discount:**
* Calculating Proceeds
* Calculating Discount Rate
* Calculating Equivalent Simple Interest Rate
* **Compound Interest:**
* Calculating Future Value (F)
* Calculating Present Value (P)
* Calculating Nominal Interest Rate (r)
* Calculating Time Period (t or n)
* **Effective Rate of Interest:**
* Calculating Effective Rate (ER) given nominal rate and compounding frequency.
* **Continuous Compounding Interest:**
* Calculating Future Value (F)
* Calculating Present Value (P)
* Calculating Nominal Interest Rate (r)
* Calculating Time Period (t)
* Calculating Equivalent Simple Interest Rate for a continuously compounded rate.
## System Architecture
The problem generator is a modular system composed of several Python scripts and JSON data files that work together to produce financial problems.
```mermaid
graph TD
A[problem_engine.py] --> B(data_loader.py);
A --> C(value_sampler.py);
A --> D(date_utils.py);
A --> E(formula_evaluator.py);
A --> F(narrative_builder.py);
A --> G(solution_presenter.py);
B -- Reads & Caches --> H[financial_concepts.json];
B -- Reads & Caches --> I[value_ranges.json];
B -- Reads & Caches --> J[text_snippets.json];
B -- Reads & Caches --> K[data/names.json];
C -- Uses Constraints From --> I;
C -- Uses Date Functions From --> D;
F -- Uses Actor Names From --> K;
F -- Uses Text Templates From --> J;
G -- Uses Text Templates From --> J;
subgraph Python Modules
A
B
C
D
E
F
G
end
subgraph Data Files (JSON)
H[building_blocks/financial_concepts.json]
I[data/value_ranges.json]
J[data/text_snippets.json]
K[data/names.json]
end
```
### Python Modules:
* **`main.py`:**
* The main entry point for running the problem generator.
* Handles initialization and calls the problem generation engine to produce and display problems.
* **`problem_engine.py`:**
* The central coordinator of the problem generation process.
* Selects a financial concept, manages the flow of data between other modules (sampling values, evaluating formulas, building narrative, presenting solution), and assembles the final problem output.
* Includes logic for handling special variable types (like dates) and basic plausibility checks (e.g., for interest rate calculations).
* Caches loaded data to improve performance.
* **`data_loader.py`:**
* Responsible for loading data from the various JSON configuration files (`financial_concepts.json`, `value_ranges.json`, `text_snippets.json`, `names.json`).
* Provides functions to access cached versions of this data.
* **`value_sampler.py`:**
* Generates random numerical values for the variables in a problem (e.g., principal, interest rate, time).
* Uses constraints (min, max, precision, type) defined in `data/value_ranges.json`.
* Includes functions to format these values for display (e.g., adding currency symbols, percentage signs, thousands separators, formatting dates).
* **`date_utils.py`:**
* Provides utility functions for date-related calculations, crucial for problems involving specific time periods (e.g., exact simple interest).
* Includes functions for leap year checks, days in month/year, generating random dates and date periods, and calculating exact days between dates.
* **`formula_evaluator.py`:**
* Safely evaluates mathematical formula strings (defined in `building_blocks/financial_concepts.json`) using a restricted Python `eval()` environment.
* Takes a formula string and a context dictionary of variables and their values, then returns the calculated result.
* **`narrative_builder.py`:**
* Constructs the natural language word problem.
* Uses templates from `data/text_snippets.json` and actor names/details from `data/names.json`.
* Combines these with the sampled numerical values (formatted by `value_sampler.py`) to create a coherent and varied problem statement.
* **`solution_presenter.py`:**
* Generates a step-by-step guided solution for the problem.
* Uses `solution_step_keys` defined in `building_blocks/financial_concepts.json` to fetch corresponding solution step templates from `data/text_snippets.json`.
* Populates these templates with the specific values and intermediate calculations for the current problem, including a step showing the formula with values substituted.
### Data Files (JSON):
* **`building_blocks/financial_concepts.json`:**
* The core data file defining each type of financial problem the system can generate.
* Each "concept" includes:
* `concept_id`: A unique identifier.
* `description`: Human-readable explanation.
* `financial_topic`: The broader category (e.g., "Simple Interest").
* `target_unknown`: The variable the problem will ask to solve for.
* `variables_involved`: All variables relevant to the concept.
* `formulas`: A dictionary of formula strings (including for intermediate variables).
* `required_knowns_for_target`: Input variables needed to solve the problem.
* `narrative_hooks`: Keywords to guide narrative construction.
* `solution_step_keys`: An ordered list of keys mapping to solution step templates in `data/text_snippets.json`.
* **`data/value_ranges.json`:**
* Defines the constraints for generating random numerical values (min, max, currency, units, precision, type like integer/float).
* Includes options for compounding frequencies and date generation parameters (like `date_period_generation`).
* **`data/text_snippets.json`:**
* A rich repository of text fragments used by `narrative_builder.py` and `solution_presenter.py`.
* Contains templates for:
* Actor descriptions (persons, companies).
* Actions (loan, investment, repayment verbs).
* Time phrases, rate phrases, compounding phrases.
* Question starters and scenario framing elements.
* Detailed templates for each step of the guided solution (e.g., identifying knowns, stating formula, substituting values).
* Human-readable descriptions for variable keys.
* **`data/names.json`:**
* Provides lists of names (titles, first names, last names for persons) and company name components (prefixes, suffixes, industry types).
* Also includes lists of items for loan/investment purposes to add flavor to narratives.
## Problem Generation Flow
1. **Initialization:** `problem_engine.py` (called by `main.py`) loads and caches all necessary data from JSON files via `data_loader.py`.
2. **Concept Selection:** A financial concept is randomly chosen from `building_blocks/financial_concepts.json`.
3. **Value Sampling:**
* For each `required_knowns_for_target` in the selected concept, `problem_engine.py` instructs `value_sampler.py` to generate a value.
* `value_sampler.py` uses `data/value_ranges.json` for constraints.
* For date-related concepts (Exact/Ordinary Simple Interest), `problem_engine.py` uses `date_utils.py` to generate start/end dates and calculate the number of days, time base, etc.
* Compounding frequencies are also sampled specifically.
* Generated values are stored along with their metadata (units, precision).
* Basic plausibility checks (e.g., ensuring F > P when solving for rate) are performed, with resampling attempts if needed.
4. **Formula Evaluation (Intermediate & Final):**
* `problem_engine.py` identifies all formulas (intermediate and target) for the concept.
* `formula_evaluator.py` evaluates these formulas sequentially, using the sampled known values and any previously calculated intermediate values.
5. **Narrative Construction:**
* `narrative_builder.py` takes the selected concept, the (formatted) known values, and the target unknown variable.
* It uses templates from `data/text_snippets.json` and names from `data/names.json` to construct a word problem.
6. **Solution Generation:**
* `solution_presenter.py` uses the `solution_step_keys` from the concept and corresponding templates from `data/text_snippets.json`.
* It populates these templates with the actual problem data (knowns, intermediates, calculated answer) to create a step-by-step solution, including showing the formula with values substituted.
7. **Output:** `problem_engine.py` returns a structured dictionary containing the problem statement, the question, all variable data, the calculated answer (raw and formatted), and the list of solution steps. `main.py` then prints this information.
## Key Features
* **Data-Driven Design:** Problem types, numerical ranges, and language are defined in external JSON files, enhancing flexibility.
* **Modularity:** Components for value sampling, formula evaluation, narrative building, and solution presentation are distinct.
* **Procedural Generation:** Ensures variety in generated problems.
* **Natural Language Output:** Aims for human-readable problem statements and solutions.
* **Step-by-Step Solutions:** Provides detailed, guided solutions, including formula substitution.
* **Precision Control:** Manages internal precision for calculations and display precision for output.
* **Date Handling:** Robust utilities for date calculations for Exact and Ordinary Simple Interest.
* **Plausibility Checks:** Basic checks to avoid some unrealistic problem scenarios (e.g., large negative rates).
## Setup and Usage
This project uses `uv` for Python environment and dependency management, as indicated by `uv.lock` and `.python-version`.
1. **Environment Setup (Recommended):**
* Ensure `uv` is installed.
* Navigate to the project root directory.
* Run `uv sync` (if `pyproject.toml` lists dependencies) or `uv pip install -r requirements.txt` (if a `requirements.txt` file exists or is generated). Refer to `pyproject.toml` for actual dependencies.
2. **Running the Generator:**
* The primary way to generate a problem is by running `main.py` from the project root directory:
```bash
python3 main.py
```
* The `src/problem_engine.py` script can also be run directly (e.g., `python3 src/problem_engine.py`). Its `if __name__ == '__main__':` block is currently configured to test all available concepts.
## Extensibility
The system is designed to be extensible for adding new problem types that fit the current architectural pattern (solving for a single target unknown via a primary formula, possibly with intermediate calculations).
* **Adding New Financial Concepts (Similar Architecture):**
1. **Define Concept:** Add a new JSON object to `building_blocks/financial_concepts.json`. Specify:
* `concept_id` (unique string)
* `description` (string)
* `financial_topic` (string, e.g., "Simple Annuity")
* `target_unknown` (string, e.g., "F_annuity")
* `variables_involved` (list of strings, all variables in the formulas)
* `formulas` (dictionary: `{"variable_to_calc": "formula_string", ...}`). Ensure formulas for intermediate variables are listed before those that depend on them.
* `required_knowns_for_target` (list of strings, input variables to be sampled)
* `narrative_hooks` (list of strings, keywords for `narrative_builder.py`)
* `solution_step_keys` (list of strings, keys from `text_snippets.json -> solution_guidance`)
2. **Value Ranges:** If new variables require specific generation rules, add entries to `data/value_ranges.json`.
3. **Text Snippets:**
* Add descriptions for any new variables to `data/text_snippets.json` under `variable_descriptions`.
* Add new solution step templates to `solution_guidance` if existing ones are insufficient, and use these new keys in your concept's `solution_step_keys`.
* Add new narrative phrase templates if needed for unique storytelling.
4. **Variable Mapping:** Update `CONCEPT_VAR_TO_VALUERANGE_KEY` in `src/problem_engine.py` if new variables need mapping to `value_ranges.json` keys for sampling.
5. **Special Handling (If Any):** If the new concept requires unique logic in `problem_engine.py` (e.g., special date handling, unique plausibility checks) or `solution_presenter.py` (unique formatting for a solution step), those modules would need to be extended.
* **Adding New Names/Items:** Edit `data/names.json` to add more variety to actors or scenario items.
* **Adding New Text Snippets:** Edit `data/text_snippets.json` to add more phrasing options for narratives or solution steps.
* **Modifying Value Ranges:** Adjust `data/value_ranges.json` to change the scope of numerical values generated.
### Future Enhancements / Scope for Advanced Concepts
The current architecture is well-suited for problems that can be solved by evaluating one or more direct formulas to find a target unknown. More complex topics from engineering economy, such as:
* **Equation of Value / Discrete Payments (with algebraic unknowns):** Problems like the "Acosta Holdings Loan" or "Mr. Cruz's Car Purchase" from the reference notes, where an unknown payment (X) appears in multiple terms of an equation of value and needs to be solved for algebraically.
* **Arithmetic/Geometric Gradients:** While the "Restaurant Lease - Arithmetic Gradient" problem involves standard formulas (P/A and P/G), integrating these as a distinct, generatable concept requires defining them with their specific variables (A1, G, N) and solution steps.
* **Annuities (Ordinary, Due, Deferred, Perpetuities):** These have their own sets of formulas and common problem structures.
* **More Advanced Topics:** Depreciation, Bonds, Capital Budgeting (NPV, IRR), etc.
Implementing these advanced topics would require significant enhancements:
1. **Equation Solving:** The current `formula_evaluator.py` evaluates expressions but doesn't solve equations algebraically. This would likely require integrating a symbolic math library (like SymPy) or developing custom logic to set up and solve equations of value.
2. **Complex Concept Definitions:** `building_blocks/financial_concepts.json` would need a more sophisticated structure to define multiple cash flows, their timings, relationships (e.g., payment2 = 1.5 * payment1), and the setup of an equation rather than just direct formulas for a target.
3. **Enhanced `problem_engine.py`:** Logic to manage multiple cash flows, select focal dates, and coordinate the setup of equations for the solver.
4. **Advanced `solution_presenter.py`:** New solution step templates and logic to explain:
* Drawing cash flow diagrams (descriptively).
* Choosing a focal date.
* Bringing each cash flow to the focal date.
* Setting up the equation of value.
* Showing algebraic manipulation to solve for the unknown.
* Specific steps for gradient series components.
5. **Richer `value_sampler.py`:** To generate series of payments, gradient parameters, or multiple related loan/payment amounts.
These represent a next level of complexity beyond the current system's direct formula evaluation approach. A `docs/` folder could be created in the future to detail potential architectures for these advanced features.
## Known Issues / Potential Refinements
* **Banker's Discount - Negative Proceeds:** For the `BANKERS_DISCOUNT_SOLVE_FOR_PROCEEDS` concept, it's possible to generate scenarios where the calculated discount amount is larger than the maturity value, resulting in negative proceeds. While mathematically possible, this is often an undesirable outcome for typical practice problems. A future refinement could involve adding more specific plausibility checks in `problem_engine.py` to resample input values (like discount rate or time) to ensure positive proceeds.
* **Plausibility of Complex Scenarios:** As more complex multi-step problems are considered (like Equation of Value), ensuring the generated numbers lead to "sensible" real-world scenarios will require more sophisticated plausibility checks during value sampling.

View File

@ -0,0 +1,489 @@
[
{
"concept_id": "SIMPLE_INTEREST_SOLVE_FOR_F",
"description": "Calculates the Future Value (F_simple) in a simple interest problem, given Principal, annual simple interest rate, and time in years.",
"financial_topic": "Simple Interest",
"target_unknown": "F_simple",
"variables_involved": ["P", "i_simple_annual", "n_time_years", "F_simple"],
"formulas": {
"F_simple": "P * (1 + i_simple_annual * n_time_years)"
},
"required_knowns_for_target": ["P", "i_simple_annual", "n_time_years"],
"narrative_hooks": ["loan", "investment", "borrow", "deposit", "future amount", "accumulated value", "simple interest", "maturity value"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.convert_time_to_years",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "SIMPLE_INTEREST_SOLVE_FOR_P_FROM_F",
"description": "Calculates the Principal (P) in a simple interest problem, given Future Value, annual simple interest rate, and time in years.",
"financial_topic": "Simple Interest",
"target_unknown": "P",
"variables_involved": ["P", "i_simple_annual", "n_time_years", "F_simple"],
"formulas": {
"P": "F_simple / (1 + i_simple_annual * n_time_years)"
},
"required_knowns_for_target": ["F_simple", "i_simple_annual", "n_time_years"],
"narrative_hooks": ["loan", "investment", "present worth", "initial amount", "deposit now", "simple interest"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.convert_time_to_years",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "SIMPLE_INTEREST_SOLVE_FOR_I",
"description": "Calculates the simple Interest amount (I_simple), given Principal, annual simple interest rate, and time in years.",
"financial_topic": "Simple Interest",
"target_unknown": "I_simple",
"variables_involved": ["P", "i_simple_annual", "n_time_years", "I_simple"],
"formulas": {
"I_simple": "P * i_simple_annual * n_time_years"
},
"required_knowns_for_target": ["P", "i_simple_annual", "n_time_years"],
"narrative_hooks": ["interest earned", "interest due", "cost of borrowing", "simple interest"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.convert_time_to_years",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "SIMPLE_INTEREST_SOLVE_FOR_P_FROM_I",
"description": "Calculates the Principal (P) in a simple interest problem, given Interest amount, annual simple interest rate, and time in years.",
"financial_topic": "Simple Interest",
"target_unknown": "P",
"variables_involved": ["P", "i_simple_annual", "n_time_years", "I_simple"],
"formulas": {
"P": "I_simple / (i_simple_annual * n_time_years)"
},
"required_knowns_for_target": ["I_simple", "i_simple_annual", "n_time_years"],
"narrative_hooks": ["principal amount", "original loan", "initial investment", "simple interest", "interest yielded"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.convert_time_to_years",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "SIMPLE_INTEREST_SOLVE_FOR_RATE_FROM_I",
"description": "Calculates the annual simple interest rate (i_simple_annual), given Principal, Interest amount, and time in years.",
"financial_topic": "Simple Interest",
"target_unknown": "i_simple_annual",
"variables_involved": ["P", "i_simple_annual", "n_time_years", "I_simple"],
"formulas": {
"i_simple_annual": "I_simple / (P * n_time_years)"
},
"required_knowns_for_target": ["P", "I_simple", "n_time_years"],
"narrative_hooks": ["interest rate", "rate of return", "annual rate", "simple interest"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.convert_time_to_years",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "SIMPLE_INTEREST_SOLVE_FOR_TIME_FROM_I",
"description": "Calculates the time period in years (n_time_years), given Principal, Interest amount, and annual simple interest rate.",
"financial_topic": "Simple Interest",
"target_unknown": "n_time_years",
"variables_involved": ["P", "i_simple_annual", "n_time_years", "I_simple"],
"formulas": {
"n_time_years": "I_simple / (P * i_simple_annual)"
},
"required_knowns_for_target": ["P", "I_simple", "i_simple_annual"],
"narrative_hooks": ["time period", "duration", "loan term", "investment horizon", "simple interest"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "COMPOUND_INTEREST_SOLVE_FOR_F",
"description": "Calculates the Future Value (F_compound) in a compound interest problem, given Principal, nominal annual interest rate, compounding frequency, and time in years.",
"financial_topic": "Compound Interest",
"target_unknown": "F_compound",
"variables_involved": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years", "i_rate_per_period", "n_total_compounding_periods", "F_compound"],
"formulas": {
"i_rate_per_period": "r_nominal_annual / m_compounding_periods_per_year",
"n_total_compounding_periods": "t_years * m_compounding_periods_per_year",
"F_compound": "P * (1 + i_rate_per_period)**n_total_compounding_periods"
},
"required_knowns_for_target": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years"],
"narrative_hooks": ["investment", "deposit", "loan", "future worth", "accumulated amount", "compound interest", "compounded"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.calculate_interest_rate_per_period",
"solution_guidance.calculate_total_periods",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "COMPOUND_INTEREST_SOLVE_FOR_P_FROM_F",
"description": "Calculates the Principal (P) in a compound interest problem, given Future Value, nominal annual interest rate, compounding frequency, and time in years.",
"financial_topic": "Compound Interest",
"target_unknown": "P",
"variables_involved": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years", "i_rate_per_period", "n_total_compounding_periods", "F_compound"],
"formulas": {
"i_rate_per_period": "r_nominal_annual / m_compounding_periods_per_year",
"n_total_compounding_periods": "t_years * m_compounding_periods_per_year",
"P": "F_compound / (1 + i_rate_per_period)**n_total_compounding_periods"
},
"required_knowns_for_target": ["F_compound", "r_nominal_annual", "m_compounding_periods_per_year", "t_years"],
"narrative_hooks": ["present value", "initial investment", "deposit now", "amount to invest", "compound interest", "compounded"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.calculate_interest_rate_per_period",
"solution_guidance.calculate_total_periods",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "COMPOUND_INTEREST_SOLVE_FOR_RATE",
"description": "Calculates the nominal annual interest rate (r_nominal_annual), given Principal, Future Value, compounding frequency, and time in years.",
"financial_topic": "Compound Interest",
"target_unknown": "r_nominal_annual",
"variables_involved": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years", "i_rate_per_period", "n_total_compounding_periods", "F_compound"],
"formulas": {
"n_total_compounding_periods": "t_years * m_compounding_periods_per_year",
"i_rate_per_period": "(F_compound / P)**(1 / n_total_compounding_periods) - 1",
"r_nominal_annual": "i_rate_per_period * m_compounding_periods_per_year"
},
"required_knowns_for_target": ["P", "F_compound", "m_compounding_periods_per_year", "t_years"],
"narrative_hooks": ["interest rate", "nominal rate", "rate of return", "compound interest", "compounded"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.calculate_total_periods",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.intermediate_step",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "COMPOUND_INTEREST_SOLVE_FOR_TIME",
"description": "Calculates the time in years (t_years), given Principal, Future Value, nominal annual interest rate, and compounding frequency. Requires math.log.",
"financial_topic": "Compound Interest",
"target_unknown": "t_years",
"variables_involved": ["P", "r_nominal_annual", "m_compounding_periods_per_year", "t_years", "i_rate_per_period", "n_total_compounding_periods", "F_compound"],
"formulas": {
"i_rate_per_period": "r_nominal_annual / m_compounding_periods_per_year",
"n_total_compounding_periods": "math.log(F_compound / P) / math.log(1 + i_rate_per_period)",
"t_years": "n_total_compounding_periods / m_compounding_periods_per_year"
},
"required_knowns_for_target": ["P", "F_compound", "r_nominal_annual", "m_compounding_periods_per_year"],
"narrative_hooks": ["time period", "duration", "investment horizon", "how long", "compound interest", "compounded"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.calculate_interest_rate_per_period",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.intermediate_step",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "EFFECTIVE_RATE_SOLVE_FOR_ER",
"description": "Calculates the Effective Annual Interest Rate (ER), given the nominal annual interest rate and the number of compounding periods per year.",
"financial_topic": "Effective Rate of Interest",
"target_unknown": "ER",
"variables_involved": ["r_nominal_annual", "m_compounding_periods_per_year", "i_rate_per_period", "ER"],
"formulas": {
"i_rate_per_period": "r_nominal_annual / m_compounding_periods_per_year",
"ER": "(1 + i_rate_per_period)**m_compounding_periods_per_year - 1"
},
"required_knowns_for_target": ["r_nominal_annual", "m_compounding_periods_per_year"],
"narrative_hooks": ["effective rate", "actual annual rate", "true interest rate", "compounding effect"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.calculate_interest_rate_per_period",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "CONTINUOUS_COMPOUNDING_SOLVE_FOR_F",
"description": "Calculates the Future Value (F_continuous) with continuous compounding, given Principal, nominal annual interest rate, and time in years. Requires math.exp.",
"financial_topic": "Continuous Compounding Interest",
"target_unknown": "F_continuous",
"variables_involved": ["P", "r_nominal_annual", "t_years", "F_continuous"],
"formulas": {
"F_continuous": "P * math.exp(r_nominal_annual * t_years)"
},
"required_knowns_for_target": ["P", "r_nominal_annual", "t_years"],
"narrative_hooks": ["continuous compounding", "investment growth", "future value", "accumulated amount"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "CONTINUOUS_COMPOUNDING_SOLVE_FOR_P_FROM_F",
"description": "Calculates the Principal (P) with continuous compounding, given Future Value, nominal annual interest rate, and time in years. Requires math.exp.",
"financial_topic": "Continuous Compounding Interest",
"target_unknown": "P",
"variables_involved": ["P", "r_nominal_annual", "t_years", "F_continuous"],
"formulas": {
"P": "F_continuous / math.exp(r_nominal_annual * t_years)"
},
"required_knowns_for_target": ["F_continuous", "r_nominal_annual", "t_years"],
"narrative_hooks": ["continuous compounding", "present value", "initial investment", "amount to deposit"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "CONTINUOUS_COMPOUNDING_SOLVE_FOR_RATE",
"description": "Calculates the nominal annual interest rate (r_nominal_annual) with continuous compounding, given Principal, Future Value, and time in years. Requires math.log.",
"financial_topic": "Continuous Compounding Interest",
"target_unknown": "r_nominal_annual",
"variables_involved": ["P", "r_nominal_annual", "t_years", "F_continuous"],
"formulas": {
"r_nominal_annual": "math.log(F_continuous / P) / t_years"
},
"required_knowns_for_target": ["P", "F_continuous", "t_years"],
"narrative_hooks": ["continuous compounding", "interest rate", "nominal rate", "rate of growth"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "CONTINUOUS_COMPOUNDING_SOLVE_FOR_TIME",
"description": "Calculates the time in years (t_years) with continuous compounding, given Principal, Future Value, and nominal annual interest rate. Requires math.log.",
"financial_topic": "Continuous Compounding Interest",
"target_unknown": "t_years",
"variables_involved": ["P", "r_nominal_annual", "t_years", "F_continuous"],
"formulas": {
"t_years": "math.log(F_continuous / P) / r_nominal_annual"
},
"required_knowns_for_target": ["P", "F_continuous", "r_nominal_annual"],
"narrative_hooks": ["continuous compounding", "time period", "duration", "how long to reach value"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "CONTINUOUS_COMPOUNDING_EQUIVALENT_SIMPLE_RATE",
"description": "Calculates the equivalent simple interest rate for 1 year for a given nominal annual rate compounded continuously. Requires math.exp.",
"financial_topic": "Continuous Compounding Interest",
"target_unknown": "i_simple_equivalent",
"variables_involved": ["r_nominal_annual", "i_simple_equivalent"],
"formulas": {
"i_simple_equivalent": "math.exp(r_nominal_annual) - 1"
},
"required_knowns_for_target": ["r_nominal_annual"],
"narrative_hooks": ["continuous compounding", "equivalent simple rate", "comparison rate"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "BANKERS_DISCOUNT_SOLVE_FOR_PROCEEDS",
"description": "Calculates the Proceeds (P_proceeds) in a Banker's Discount problem, given Maturity Value (F_maturity), discount rate, and time.",
"financial_topic": "Banker's Discount",
"target_unknown": "P_proceeds",
"variables_involved": ["F_maturity", "d_discount_rate", "t_years", "Db_discount_amount", "P_proceeds"],
"formulas": {
"Db_discount_amount": "F_maturity * d_discount_rate * t_years",
"P_proceeds": "F_maturity - Db_discount_amount"
},
"required_knowns_for_target": ["F_maturity", "d_discount_rate", "t_years"],
"narrative_hooks": ["banker's discount", "discounted loan", "proceeds", "amount received", "maturity value", "face value"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.convert_time_to_years",
"solution_guidance.state_formula",
"solution_guidance.intermediate_step",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "BANKERS_DISCOUNT_SOLVE_FOR_DISCOUNT_RATE",
"description": "Calculates the discount rate (d_discount_rate) in a Banker's Discount problem, given Maturity Value, Proceeds, and time.",
"financial_topic": "Banker's Discount",
"target_unknown": "d_discount_rate",
"variables_involved": ["F_maturity", "d_discount_rate", "t_years", "Db_discount_amount", "P_proceeds"],
"formulas": {
"Db_discount_amount": "F_maturity - P_proceeds",
"d_discount_rate": "Db_discount_amount / (F_maturity * t_years)"
},
"required_knowns_for_target": ["F_maturity", "P_proceeds", "t_years"],
"narrative_hooks": ["banker's discount", "discount rate", "rate of discount", "proceeds", "maturity value"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.convert_time_to_years",
"solution_guidance.intermediate_step",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "BANKERS_DISCOUNT_SOLVE_FOR_SIMPLE_INTEREST_EQUIVALENT",
"description": "Calculates the equivalent simple interest rate (i_simple_equivalent) for a Banker's Discount scenario, based on Proceeds.",
"financial_topic": "Banker's Discount",
"target_unknown": "i_simple_equivalent",
"variables_involved": ["F_maturity", "t_years", "Db_discount_amount", "P_proceeds", "i_simple_equivalent"],
"formulas": {
"Db_discount_amount": "F_maturity - P_proceeds",
"i_simple_equivalent": "Db_discount_amount / (P_proceeds * t_years)"
},
"required_knowns_for_target": ["F_maturity", "P_proceeds", "t_years"],
"narrative_hooks": ["banker's discount", "equivalent simple interest", "comparison rate", "true cost of borrowing"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.convert_time_to_years",
"solution_guidance.intermediate_step",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "EXACT_SIMPLE_INTEREST_SOLVE_FOR_I",
"description": "Calculates the Exact Simple Interest amount (I_exact_simple), given Principal, annual simple interest rate, start date, and end date.",
"financial_topic": "Exact Simple Interest",
"target_unknown": "I_exact_simple",
"variables_involved": ["P", "i_simple_annual", "start_date", "end_date", "n_time_days", "time_base_days", "n_time_years_fractional", "I_exact_simple"],
"formulas": {
"n_time_years_fractional": "n_time_days / time_base_days",
"I_exact_simple": "P * i_simple_annual * n_time_years_fractional"
},
"required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"],
"narrative_hooks": ["exact simple interest", "loan interest", "investment earnings", "specific period"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.days_in_period",
"solution_guidance.check_leap_year",
"solution_guidance.determine_time_base_exact",
"solution_guidance.calculate_n_time_years_fractional",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "EXACT_SIMPLE_INTEREST_SOLVE_FOR_F",
"description": "Calculates the Future Value (F_exact_simple) with Exact Simple Interest, given Principal, annual simple interest rate, start date, and end date.",
"financial_topic": "Exact Simple Interest",
"target_unknown": "F_exact_simple",
"variables_involved": ["P", "i_simple_annual", "start_date", "end_date", "n_time_days", "time_base_days", "n_time_years_fractional", "F_exact_simple"],
"formulas": {
"n_time_years_fractional": "n_time_days / time_base_days",
"F_exact_simple": "P * (1 + i_simple_annual * n_time_years_fractional)"
},
"required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"],
"narrative_hooks": ["exact simple interest", "maturity value", "future amount", "specific period"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.days_in_period",
"solution_guidance.check_leap_year",
"solution_guidance.determine_time_base_exact",
"solution_guidance.calculate_n_time_years_fractional",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "ORDINARY_SIMPLE_INTEREST_SOLVE_FOR_I",
"description": "Calculates the Ordinary Simple Interest amount (I_ordinary_simple), given Principal, annual simple interest rate, start date, and end date.",
"financial_topic": "Ordinary Simple Interest",
"target_unknown": "I_ordinary_simple",
"variables_involved": ["P", "i_simple_annual", "start_date", "end_date", "n_time_days", "time_base_days", "n_time_years_fractional", "I_ordinary_simple"],
"formulas": {
"n_time_years_fractional": "n_time_days / time_base_days",
"I_ordinary_simple": "P * i_simple_annual * n_time_years_fractional"
},
"required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"],
"narrative_hooks": ["ordinary simple interest", "loan interest", "investment earnings", "360 day year"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.days_in_period",
"solution_guidance.determine_time_base_ordinary",
"solution_guidance.calculate_n_time_years_fractional",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
},
{
"concept_id": "ORDINARY_SIMPLE_INTEREST_SOLVE_FOR_F",
"description": "Calculates the Future Value (F_ordinary_simple) with Ordinary Simple Interest, given Principal, annual simple interest rate, start date, and end date.",
"financial_topic": "Ordinary Simple Interest",
"target_unknown": "F_ordinary_simple",
"variables_involved": ["P", "i_simple_annual", "start_date", "end_date", "n_time_days", "time_base_days", "n_time_years_fractional", "F_ordinary_simple"],
"formulas": {
"n_time_years_fractional": "n_time_days / time_base_days",
"F_ordinary_simple": "P * (1 + i_simple_annual * n_time_years_fractional)"
},
"required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"],
"narrative_hooks": ["ordinary simple interest", "maturity value", "future amount", "360 day year"],
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.days_in_period",
"solution_guidance.determine_time_base_ordinary",
"solution_guidance.calculate_n_time_years_fractional",
"solution_guidance.state_formula",
"solution_guidance.substitute_values",
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
}
]

69
data/names.json Normal file
View File

@ -0,0 +1,69 @@
{
"persons_titles": ["Mr.", "Ms.", "Engr.", "Dr.", "Prof."],
"persons_first_names": [
"James", "Mary", "John", "Patricia", "Robert", "Jennifer", "Michael", "Linda",
"William", "Elizabeth", "David", "Barbara", "Richard", "Susan", "Joseph", "Jessica",
"Thomas", "Sarah", "Charles", "Karen", "Christopher", "Nancy", "Daniel", "Lisa",
"Matthew", "Betty", "Anthony", "Margaret", "Mark", "Sandra", "Juan", "Maria",
"Jose", "Ana", "Carlos", "Sofia"
],
"persons_last_names": [
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
"Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson",
"Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee", "Perez", "Thompson",
"White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker",
"Young", "Allen", "King", "Wright", "Scott", "Green", "Baker", "Adams", "Nelson",
"Hill", "Campbell", "Mitchell", "Roberts", "Carter", "Phillips", "Evans", "Turner",
"Torres", "Parker", "Collins", "Edwards", "Stewart", "Flores", "Morris", "Nguyen",
"Murphy", "Rivera", "Cook", "Rogers", "Morgan", "Peterson", "Cooper", "Reed",
"Bailey", "Bell", "Gomez", "Kelly", "Howard", "Ward", "Cox", "Diaz", "Richardson",
"Wood", "Watson", "Brooks", "Bennett", "Gray", "James", "Reyes", "Cruz", "Hughes",
"Price", "Myers", "Long", "Foster", "Sanders", "Ross", "Morales", "Powell",
"Sullivan", "Russell", "Ortiz", "Jenkins", "Gutierrez", "Perry", "Butler",
"Barnes", "Fisher", "Henderson", "Coleman", "Simmons", "Patterson", "Jordan",
"Reynolds", "Hamilton", "Graham", "Kim", "Gonzales", "Alexander", "Ramos",
"Wallace", "Griffin", "West", "Cole", "Hayes", "Chavez", "Gibson", "Bryant",
"Ellis", "Stevens", "Murray", "Ford", "Marshall", "Owens", "McDonald", "Harrison",
"Ruiz", "Kennedy", "Wells", "Alvarez", "Woods", "Mendoza", "Castillo", "Olson",
"Webb", "Washington", "Tucker", "Freeman", "Burns", "Henry", "Vasquez", "Snyder",
"Simpson", "Crawford", "Jimenez", "Porter", "Mason", "Shaw", "Gordon", "Wagner",
"Hunter", "Romero", "Hicks", "Dixon", "Hunt", "Palmer", "Robertson", "Black",
"Holmes", "Stone", "Meyer", "Boyd", "Mills", "Warren", "Fox", "Rose", "Rice",
"Moreno", "Schmidt", "Patel", "Ferguson", "Nichols", "Herrera", "Medina",
"Ryan", "Fernandez", "Weaver", "Daniels", "Stephens", "Gardner", "Payne",
"Kelley", "Dunn", "Pierce", "Arnold", "Tran", "Spencer", "Peters", "Hawkins",
"Grant", "Hansen", "Castro", "Hoffman", "Hart", "Elliott", "Cunningham",
"Knight", "Bradley", "Santos"
],
"companies_generic_prefix": [
"Apex", "Vertex", "Zenith", "Nova", "Orion", "Quantum", "Synergy", "Global",
"United", "Dynamic", "Prime", "Core", "Alpha", "Beta", "Omega", "Delta",
"Stellar", "Pinnacle", "Summit", "Horizon", "Matrix", "Nexus", "Catalyst"
],
"companies_generic_suffix": [
"Solutions", "Enterprises", "Group", "Corp.", "Inc.", "LLC", "Technologies",
"Systems", "Dynamics", "Innovations", "Ventures", "Holdings", "Logistics",
"Manufacturing", "Consulting", "Services", "Industries", "Partners"
],
"companies_industry_specific": {
"finance": ["Capital", "Financial", "Investment Group", "Bank", "Credit Union"],
"tech": ["Tech", "Software", "Digital", "Cybernetics", "AI Solutions"],
"construction": ["Builders", "Construction Co.", "Development", "Contractors"],
"retail": ["Goods", "Emporium", "Mart", "Retailers", "Supply Co."]
},
"items_loan_general": [
"a business expansion", "a new equipment purchase", "a property acquisition",
"working capital", "a personal project", "debt consolidation", "a vehicle",
"home improvement", "education fees", "a startup venture"
],
"items_investment_general": [
"stocks", "bonds", "a mutual fund", "real estate", "a new business",
"a savings account", "a certificate of deposit", "a retirement fund",
"a tech startup", "a portfolio of assets"
],
"project_names": [
"Project Alpha", "The Phoenix Initiative", "Operation Starlight", "Blue Sky Project",
"Quantum Leap Program", "Project Chimera", "The Vanguard Project", "Odyssey Plan",
"Project Nova", "Titan Development"
]
}

268
data/text_snippets.json Normal file
View File

@ -0,0 +1,268 @@
{
"actors_person": [
"{person_title} {person_first_name} {person_last_name}",
"{person_first_name} {person_last_name}",
"{person_title} {person_last_name}"
],
"actors_company": [
"{company_prefix} {company_suffix}",
"{company_prefix} {company_industry}"
],
"actions_loan_present_singular": [
"borrows",
"takes out a loan for",
"secures financing for",
"needs a loan of",
"is seeking a loan of"
],
"actions_loan_present_plural": [
"borrow",
"take out a loan for",
"secure financing for",
"need a loan of",
"are seeking a loan of"
],
"actions_loan_past_singular": [
"borrowed",
"took out a loan for",
"secured financing for",
"received a loan of"
],
"actions_loan_past_plural": [
"borrowed",
"took out a loan for",
"secured financing for",
"received a loan of"
],
"actions_investment_present_singular": [
"invests",
"deposits",
"puts",
"plans to invest",
"wants to deposit"
],
"actions_investment_present_plural": [
"invest",
"deposit",
"put",
"plan to invest",
"want to deposit"
],
"actions_investment_past_singular": [
"invested",
"deposited",
"put",
"made an investment of"
],
"actions_investment_past_plural": [
"invested",
"deposited",
"put",
"made an investment of"
],
"actions_repayment_present_singular": [
"repays",
"settles",
"amortizes",
"makes a payment on"
],
"actions_repayment_present_plural": [
"repay",
"settle",
"amortize",
"make a payment on"
],
"actions_repayment_past_singular": [
"repaid",
"settled",
"amortized",
"made a payment on"
],
"actions_repayment_past_plural": [
"repaid",
"settled",
"amortized",
"made a payment on"
],
"actions_receive_present_singular": [
"receives",
"obtains",
"gets",
"is due to receive"
],
"actions_receive_present_plural": [
"receive",
"obtain",
"get",
"are due to receive"
],
"actions_receive_past_singular": [
"received",
"obtained",
"got"
],
"actions_receive_past_plural": [
"received",
"obtained",
"got"
],
"actions_earn_present_singular": [
"earns",
"accumulates",
"yields"
],
"actions_earn_present_plural": [
"earn",
"accumulate",
"yield"
],
"actions_earn_past_singular": [
"earned",
"accumulated",
"yielded"
],
"actions_earn_past_plural": [
"earned",
"accumulated",
"yielded"
],
"time_phrases_duration": [
"for a period of",
"over",
"for",
"during"
],
"time_phrases_point": [
"at the end of",
"after",
"in"
],
"rate_phrases": [
"at a rate of",
"with an interest of",
"at an annual rate of",
"earning interest at"
],
"compounding_phrases": [
"compounded {compounding_frequency_adverb}",
"with {compounding_frequency_adverb} compounding"
],
"purpose_phrases_loan": [
"for {item_loan}",
"to finance {item_loan}",
"to purchase {item_loan}"
],
"purpose_phrases_investment": [
"in {item_investment}",
"into {item_investment}",
"to grow their capital through {item_investment}"
],
"question_starters_what_is": [
"What is the",
"Determine the",
"Calculate the",
"Find the",
"What will be the",
"What was the"
],
"question_starters_how_much": [
"How much is the",
"How much will be the",
"How much was the",
"How much should be"
],
"question_starters_how_long": [
"How long will it take for",
"How many years are needed for",
"What is the time period for"
],
"scenario_introductions": [
"Consider a scenario where {actor}",
"{actor} is planning to",
"Suppose {actor}",
"Imagine {actor} needs to"
],
"scenario_connectors": [
"The terms of the agreement state that",
"It is known that",
"Given that",
"Assuming that"
],
"scenario_closures_question_prefix": [
"Based on this information,",
"Therefore,",
"With these conditions,"
],
"solution_guidance": {
"identify_knowns": "First, let's identify the given values (knowns) in the problem:",
"state_formula": "The relevant formula for this problem is: {target_variable_lhs} = {formula_symbolic_rhs}",
"substitute_values": "Now, we substitute the known values: {target_variable_lhs} = {formula_with_values_rhs}",
"perform_calculation": "Performing the calculation:",
"intermediate_step": "The intermediate result for {step_name} is:",
"final_answer_is": "Therefore, the {unknown_variable_description} is:",
"convert_time_to_years": "Convert the time period to years: {original_time_value} {original_time_unit} = {converted_time_value_years} years.",
"calculate_interest_rate_per_period": "Calculate the interest rate per compounding period (i): i = r / m = {nominal_rate_decimal} / {compounding_periods_per_year} = {interest_rate_per_period_decimal}.",
"calculate_total_periods": "Calculate the total number of compounding periods (n): n = t * m = {time_in_years} years * {compounding_periods_per_year} = {total_periods} periods.",
"check_leap_year": "{year} is {is_or_is_not} a leap year.",
"days_in_period": "The number of days from {start_date} to {end_date} is {number_of_days} days.",
"determine_time_base_exact": "For exact simple interest, the time base is the actual number of days in the reference year ({year}), which is {days_in_year} days.",
"determine_time_base_ordinary": "For ordinary simple interest, the time base is 360 days.",
"calculate_n_time_years_fractional": "Calculate the time as a fraction of a year (t_fractional): t_fractional = number of days / time base = {n_time_days} / {time_base_days} = {n_time_years_fractional_value}.",
"identify_gradient_parameters": "Identify the parameters of the arithmetic gradient series:",
"state_formula_annuity_component": "The formula for the present worth of the base annuity component (P_A) is: P_A = A1 * (P/A, i, N) which is A1 * [((1 + i)^N - 1) / (i * (1 + i)^N)]",
"calculate_pv_annuity_component": "Calculating the present worth of the base annuity component (P_A):",
"state_formula_gradient_component": "The formula for the present worth of the arithmetic gradient component (P_G) is: P_G = G * (P/G, i, N) which is G * [(((1 + i)^N - (i * N) - 1) / (i^2 * (1 + i)^N))]",
"calculate_pv_gradient_component": "Calculating the present worth of the arithmetic gradient component (P_G):",
"state_formula_total_pv": "The total present worth (P_total) is the sum of the present worth of the annuity and gradient components: P_total = P_A + P_G",
"sum_pv_components": "Summing the present worth components:"
},
"variable_descriptions": {
"P": "principal amount",
"F": "future value",
"I": "interest amount",
"i_simple_annual": "annual simple interest rate",
"n_time_years": "time period in years",
"n_time_months": "time period in months",
"n_time_days": "time period in days",
"r_nominal_annual": "nominal annual interest rate",
"m_compounding_periods_per_year": "number of compounding periods per year",
"i_rate_per_period": "interest rate per compounding period",
"n_total_compounding_periods": "total number of compounding periods",
"ER": "effective interest rate",
"Db": "banker's discount amount",
"d_discount_rate": "discount rate",
"Proceeds": "proceeds from the loan",
"I_exact_simple": "exact simple interest amount",
"F_exact_simple": "future value with exact simple interest",
"I_ordinary_simple": "ordinary simple interest amount",
"F_ordinary_simple": "future value with ordinary simple interest",
"start_date": "start date of the period",
"end_date": "end date of the period",
"time_base_days": "day count basis for the year (time base)",
"n_time_years_fractional": "time period as a fraction of a year",
"A1_base_annuity": "base annuity amount",
"G_gradient_amount": "arithmetic gradient amount",
"N_periods": "number of periods",
"P_A_component": "present worth of the annuity component",
"P_G_component": "present worth of the gradient component",
"P_gradient_series": "total present worth of the arithmetic gradient series",
"I_simple": "simple interest amount",
"F_simple": "future value with simple interest",
"F_compound": "future value with compound interest",
"t_years": "time period in years (for compounding)",
"i_simple_equivalent": "equivalent simple interest rate",
"F_maturity": "maturity value (for Banker's Discount)",
"P_proceeds": "proceeds from discounted loan",
"Db_discount_amount": "banker's discount amount",
"F_continuous": "future value with continuous compounding"
},
"compounding_frequency_adverbs": {
"annually": "annually",
"semi-annually": "semi-annually",
"quarterly": "quarterly",
"monthly": "monthly",
"bi-monthly": "bi-monthly",
"semi-monthly": "semi-monthly",
"continuously": "continuously"
}
}

155
data/value_ranges.json Normal file
View File

@ -0,0 +1,155 @@
{
"principal": {
"min": 500,
"max": 200000,
"currency": "Php",
"decimals": 2,
"default_display_precision": 2
},
"loan_amount": {
"min": 1000,
"max": 500000,
"currency": "Php",
"decimals": 2,
"default_display_precision": 2
},
"investment_amount": {
"min": 100,
"max": 100000,
"currency": "Php",
"decimals": 2,
"default_display_precision": 2
},
"future_value": {
"min": 1000,
"max": 1000000,
"currency": "Php",
"decimals": 2,
"default_display_precision": 2
},
"interest_amount": {
"min": 50,
"max": 50000,
"currency": "Php",
"decimals": 2,
"default_display_precision": 2
},
"payment_amount": {
"min": 100,
"max": 50000,
"currency": "Php",
"decimals": 2,
"default_display_precision": 2
},
"simple_interest_rate_annual": {
"min": 0.02,
"max": 0.25,
"unit_display": "% per annum",
"internal_precision": 8,
"display_precision": 2
},
"compound_interest_rate_nominal": {
"min": 0.03,
"max": 0.18,
"unit_display": "%",
"internal_precision": 8,
"display_precision": 2
},
"interest_rate_per_period_effective": {
"min": 0.005,
"max": 0.05,
"unit_display": "% per period",
"internal_precision": 8,
"display_precision": 3
},
"discount_rate_bankers": {
"min": 0.05,
"max": 0.20,
"unit_display": "%",
"internal_precision": 8,
"display_precision": 2
},
"time_years": {
"min": 1,
"max": 20,
"unit": "years",
"integer": true,
"allow_fractional_if_months_also_present": true
},
"time_months": {
"min": 1,
"max": 48,
"unit": "months",
"integer": true
},
"time_days": {
"min": 10,
"max": 360,
"unit": "days",
"integer": true
},
"number_of_periods_general": {
"min": 2,
"max": 60,
"unit": "periods",
"integer": true
},
"loan_payment_count": {
"min": 2,
"max": 5,
"integer": true
},
"compounding_frequency_options": {
"annually": 1,
"semi-annually": 2,
"quarterly": 4,
"monthly": 12,
"bi-monthly": 6,
"semi-monthly": 24
},
"gradient_amount": {
"min": 100,
"max": 5000,
"currency": "Php",
"decimals": 2,
"default_display_precision": 2
},
"date_general": {
"description": "General date, used for start_date and end_date generation.",
"type": "date",
"min_year": 1990,
"max_year": 2030
},
"time_days_exact_ordinary": {
"description": "Number of days for exact/ordinary interest calculations (derived from dates).",
"type": "integer",
"min": 30,
"max": 730,
"unit": "days",
"display_precision": 0
},
"time_base_days_exact_ordinary": {
"description": "Time base in days for exact (365/366) or ordinary (360) interest.",
"type": "integer",
"options": [
360,
365,
366
],
"unit": "days",
"display_precision": 0
},
"time_years_fractional": {
"description": "Time period as a fraction of a year (calculated).",
"type": "float",
"unit": "years",
"display_precision": 6
},
"date_period_generation": {
"description": "Parameters for generating random date periods.",
"min_days": 30,
"max_days": 730,
"base_year_start": 1990,
"base_year_end": 2030
}
}

57
main.py Normal file
View File

@ -0,0 +1,57 @@
import sys
import os
# Ensure the src directory is in the Python path
# This allows running main.py from the project root (e.g., python main.py)
# and having imports like 'from src import ...' work correctly.
project_root = os.path.abspath(os.path.dirname(__file__))
src_path = os.path.join(project_root, 'src')
if src_path not in sys.path:
sys.path.insert(0, src_path)
if project_root not in sys.path: # Also add project root for 'from src import ...' if main is outside src
sys.path.insert(0, project_root)
from src.problem_engine import generate_problem
from src.data_loader import get_text_snippets # For variable descriptions
# Note: problem_engine.generate_problem() handles caching of all necessary data internally.
def main():
print("Generating a financial problem via main.py...\n")
# Load text snippets for variable descriptions
# generate_problem() will load its own cache, but for descriptions here, we need it too.
# A more advanced setup might pass TEXT_SNIPPETS_DATA around or make it a singleton.
text_snippets_data = get_text_snippets()
if not text_snippets_data:
print("Error: Failed to load text snippets for main.py. Exiting.")
return
problem = generate_problem()
if "error" in problem:
print(f"Error generating problem: {problem['error']}")
else:
print(f"--- Problem ---")
print(f"Concept ID: {problem['concept_id']}")
print(f"Topic: {problem['topic']}")
print("\nProblem Statement:")
print(problem['problem_statement'])
input("\nPress Enter to see the question and solution...") # Wait for user interaction
unknown_key_desc = text_snippets_data.get("variable_descriptions", {}).get(problem['target_unknown_key'], problem['target_unknown_key'])
print(f"\nQuestion: What is the {unknown_key_desc}?")
print("\nGuided Solution:")
for i, step in enumerate(problem['solution_steps']):
# The steps from solution_presenter already have numbering.
# If not, add: print(f" {i+1}. {step}")
print(step)
print(f"\nFinal Answer ({problem['target_unknown_key']}): {problem['calculated_answer_formatted']}")
print("---------------------------------------\n")
if __name__ == "__main__":
main()

7
pyproject.toml Normal file
View File

@ -0,0 +1,7 @@
[project]
name = "problem-generator"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.9"
dependencies = []

63
src/data_loader.py Normal file
View File

@ -0,0 +1,63 @@
import json
import os
DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data')
BUILDING_BLOCKS_DIR = os.path.join(os.path.dirname(__file__), '..', 'building_blocks')
def load_json_file(file_path):
"""Loads a JSON file from the given path."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data
except FileNotFoundError:
print(f"Error: File not found at {file_path}")
return None
except json.JSONDecodeError:
print(f"Error: Could not decode JSON from {file_path}")
return None
except Exception as e:
print(f"An unexpected error occurred while loading {file_path}: {e}")
return None
def get_value_ranges():
"""Loads value_ranges.json data."""
file_path = os.path.join(DATA_DIR, 'value_ranges.json')
return load_json_file(file_path)
def get_names_data():
"""Loads names.json data."""
file_path = os.path.join(DATA_DIR, 'names.json')
return load_json_file(file_path)
def get_text_snippets():
"""Loads text_snippets.json data."""
file_path = os.path.join(DATA_DIR, 'text_snippets.json')
return load_json_file(file_path)
def get_financial_concepts():
"""Loads financial_concepts.json data."""
file_path = os.path.join(BUILDING_BLOCKS_DIR, 'financial_concepts.json')
return load_json_file(file_path)
if __name__ == '__main__':
# Test functions
value_ranges = get_value_ranges()
if value_ranges:
print("Successfully loaded value_ranges.json")
# print(json.dumps(value_ranges, indent=2))
names_data = get_names_data()
if names_data:
print("Successfully loaded names.json")
# print(json.dumps(names_data, indent=2))
text_snippets = get_text_snippets()
if text_snippets:
print("Successfully loaded text_snippets.json")
# print(json.dumps(text_snippets, indent=2))
financial_concepts = get_financial_concepts()
if financial_concepts:
print("Successfully loaded financial_concepts.json")
# print(json.dumps(financial_concepts, indent=2))

133
src/date_utils.py Normal file
View File

@ -0,0 +1,133 @@
from datetime import date, timedelta
import random
def is_leap_year(year):
"""Determines if a year is a leap year."""
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
def days_in_year(year):
"""Returns the number of days in a given year."""
return 366 if is_leap_year(year) else 365
def days_in_month(year, month):
"""Returns the number of days in a specific month of a year."""
if month == 2:
return 29 if is_leap_year(year) else 28
elif month in [4, 6, 9, 11]:
return 30
else:
return 31
def get_random_date(start_year=2000, end_year=2030):
"""Generates a random date within a given year range."""
year = random.randint(start_year, end_year)
month = random.randint(1, 12)
day = random.randint(1, days_in_month(year, month))
return date(year, month, day)
def get_random_date_period(min_days=30, max_days=730, base_year_range=(1990, 2025)):
"""
Generates a random start date and an end date that is min_days to max_days after the start date.
Returns (start_date, end_date, number_of_days).
"""
start_date = get_random_date(base_year_range[0], base_year_range[1])
num_days_in_period = random.randint(min_days, max_days)
end_date = start_date + timedelta(days=num_days_in_period)
# The actual number of days between two dates, inclusive of start and exclusive of end,
# or inclusive of both if counting "duration". For interest calculations, it's often
# the difference as calculated by date objects.
# For exact simple interest, the problem usually implies counting the actual days in the period.
# timedelta already gives the difference. If the problem implies "from date X to date Y inclusive",
# then it might be (end_date - start_date).days + 1.
# However, the sample problems (e.g. Dec 27 to Mar 23) count days like:
# Dec: 31-27 = 4. Jan: 31. Feb: 28. Mar: 23. Total = 86.
# (date(2003,3,23) - date(2002,12,27)).days = 86. So timedelta is correct.
actual_days_difference = (end_date - start_date).days
return start_date, end_date, actual_days_difference
def calculate_exact_days(start_date_obj, end_date_obj):
"""
Calculates the exact number of days between two date objects.
This is consistent with how financial day counts are often done.
"""
if not isinstance(start_date_obj, date) or not isinstance(end_date_obj, date):
raise ValueError("Inputs must be datetime.date objects.")
if start_date_obj > end_date_obj:
raise ValueError("Start date cannot be after end date.")
return (end_date_obj - start_date_obj).days
def format_date_for_display(date_obj):
"""Formats a date object as 'Month Day, Year' (e.g., January 1, 2023)."""
if not isinstance(date_obj, date):
return str(date_obj)
return date_obj.strftime("%B %d, %Y")
if __name__ == '__main__':
print("Testing Date Utilities:")
# Test is_leap_year
print(f"\n--- Testing is_leap_year ---")
print(f"Is 2000 a leap year? {is_leap_year(2000)} (Expected: True)")
assert is_leap_year(2000)
print(f"Is 1900 a leap year? {is_leap_year(1900)} (Expected: False)")
assert not is_leap_year(1900)
print(f"Is 2023 a leap year? {is_leap_year(2023)} (Expected: False)")
assert not is_leap_year(2023)
print(f"Is 2024 a leap year? {is_leap_year(2024)} (Expected: True)")
assert is_leap_year(2024)
# Test days_in_year
print(f"\n--- Testing days_in_year ---")
print(f"Days in 2023: {days_in_year(2023)} (Expected: 365)")
assert days_in_year(2023) == 365
print(f"Days in 2024: {days_in_year(2024)} (Expected: 366)")
assert days_in_year(2024) == 366
# Test days_in_month
print(f"\n--- Testing days_in_month ---")
print(f"Days in Feb 2023: {days_in_month(2023, 2)} (Expected: 28)")
assert days_in_month(2023, 2) == 28
print(f"Days in Feb 2024: {days_in_month(2024, 2)} (Expected: 29)")
assert days_in_month(2024, 2) == 29
print(f"Days in Apr 2023: {days_in_month(2023, 4)} (Expected: 30)")
assert days_in_month(2023, 4) == 30
print(f"Days in Jan 2023: {days_in_month(2023, 1)} (Expected: 31)")
assert days_in_month(2023, 1) == 31
# Test get_random_date
print(f"\n--- Testing get_random_date ---")
for _ in range(3):
rd = get_random_date(2020, 2022)
print(f"Random date: {rd} ({format_date_for_display(rd)})")
assert 2020 <= rd.year <= 2022
# Test get_random_date_period
print(f"\n--- Testing get_random_date_period ---")
for _ in range(3):
start, end, num_days = get_random_date_period(min_days=10, max_days=90)
print(f"Period: {format_date_for_display(start)} to {format_date_for_display(end)}, Days: {num_days}")
assert 10 <= num_days <= 90
assert (end - start).days == num_days
# Test calculate_exact_days (matches sample problem from notes)
# Problem 3: December 27, 2002 to March 23, 2003 -> 86 days
print(f"\n--- Testing calculate_exact_days (Sample Problem) ---")
d1 = date(2002, 12, 27)
d2 = date(2003, 3, 23)
exact_days_sample = calculate_exact_days(d1, d2)
print(f"Days from {format_date_for_display(d1)} to {format_date_for_display(d2)}: {exact_days_sample} (Expected: 86)")
assert exact_days_sample == 86
# Problem 5: February 14, 1984 to November 30, 1984 -> 290 days (1984 is leap)
d3 = date(1984, 2, 14)
d4 = date(1984, 11, 30)
exact_days_sample2 = calculate_exact_days(d3, d4)
print(f"Days from {format_date_for_display(d3)} to {format_date_for_display(d4)}: {exact_days_sample2} (Expected: 290)")
assert exact_days_sample2 == 290
print("\nAll date utility tests passed if no assertion errors.")

123
src/formula_evaluator.py Normal file
View File

@ -0,0 +1,123 @@
import math
import decimal
# Define a context for safe evaluation
# Only include necessary math functions and constants
SAFE_MATH_CONTEXT = {
"math": math, # Provides access to math.log, math.exp, math.pow, etc.
"Decimal": decimal.Decimal, # For precise arithmetic if needed
# Standard operators (+, -, *, /, **) are inherently available.
# Built-in functions like round() are also available.
}
def evaluate_formula(formula_str, context_vars):
"""
Safely evaluates a mathematical formula string using a given context of variables.
Args:
formula_str (str): The formula to evaluate (e.g., "P * (1 + i * n)").
context_vars (dict): A dictionary of variable names and their numerical values
(e.g., {"P": 1000, "i": 0.05, "n": 2}).
Returns:
The result of the evaluation, or None if an error occurs.
"""
if not isinstance(formula_str, str):
print(f"Error: Formula string must be a string, got {type(formula_str)}")
return None
if not isinstance(context_vars, dict):
print(f"Error: Context variables must be a dictionary, got {type(context_vars)}")
return None
# Combine the safe math context with the problem-specific variables
# Problem-specific variables can override items in SAFE_MATH_CONTEXT if names clash,
# but this is generally not expected for simple variable names like P, F, i, n.
evaluation_context = {**SAFE_MATH_CONTEXT, **context_vars}
try:
# Using eval() here, which is generally risky if formula_str is from an untrusted source.
# However, in this application, formula_str comes from our own controlled
# financial_concepts.json file, and evaluation_context is restricted.
result = eval(formula_str, {"__builtins__": {}}, evaluation_context)
# Ensure result is a standard float or int if it's a Decimal, for consistency
if isinstance(result, decimal.Decimal):
return float(result)
return result
except NameError as e:
print(f"Error evaluating formula '{formula_str}': Variable not defined - {e}")
print(f"Available context keys: {list(evaluation_context.keys())}")
return None
except TypeError as e:
print(f"Error evaluating formula '{formula_str}': Type error - {e}")
return None
except ZeroDivisionError:
print(f"Error evaluating formula '{formula_str}': Division by zero.")
return None
except Exception as e:
print(f"An unexpected error occurred while evaluating formula '{formula_str}': {e}")
return None
if __name__ == '__main__':
print("Testing Formula Evaluator:")
# Test Case 1: Simple Interest Future Value
formula1 = "P * (1 + i * n)"
context1 = {"P": 1000, "i": 0.05, "n": 2}
result1 = evaluate_formula(formula1, context1)
print(f"\nFormula: {formula1}, Context: {context1}")
print(f"Expected: {1000 * (1 + 0.05 * 2)}")
print(f"Actual: {result1}")
assert result1 == 1100.0
# Test Case 2: Compound Interest Future Value
formula2 = "P * (1 + i)**n"
context2 = {"P": 5000, "i": 0.02, "n": 10} # e.g. 8% quarterly for 2.5 years -> i=0.08/4=0.02, n=2.5*4=10
result2 = evaluate_formula(formula2, context2)
print(f"\nFormula: {formula2}, Context: {context2}")
expected2 = 5000 * (1 + 0.02)**10
print(f"Expected: {expected2}")
print(f"Actual: {result2}")
assert abs(result2 - expected2) < 1e-9 # Compare floats with tolerance
# Test Case 3: Using math.log (e.g., solving for n in compound interest)
# n = math.log(F / P) / math.log(1 + i)
formula3 = "math.log(F / P) / math.log(1 + i)"
context3 = {"F": 6094.972103200001, "P": 5000, "i": 0.02}
result3 = evaluate_formula(formula3, context3)
print(f"\nFormula: {formula3}, Context: {context3}")
expected3 = math.log(6094.972103200001 / 5000) / math.log(1 + 0.02)
print(f"Expected: {expected3}") # Should be close to 10
print(f"Actual: {result3}")
assert abs(result3 - 10.0) < 1e-9
# Test Case 4: Using math.exp (e.g., continuous compounding)
# F = P * math.exp(r * t)
formula4 = "P * math.exp(r * t)"
context4 = {"P": 100, "r": 0.05, "t": 1}
result4 = evaluate_formula(formula4, context4)
print(f"\nFormula: {formula4}, Context: {context4}")
expected4 = 100 * math.exp(0.05 * 1)
print(f"Expected: {expected4}")
print(f"Actual: {result4}")
assert abs(result4 - expected4) < 1e-9
# Test Case 5: Undefined variable
formula5 = "P * (1 + interest_rate * n)" # 'interest_rate' not in context
context5 = {"P": 1000, "i": 0.05, "n": 2}
result5 = evaluate_formula(formula5, context5)
print(f"\nFormula: {formula5}, Context: {context5}")
print(f"Expected: None (due to error)")
print(f"Actual: {result5}")
assert result5 is None
# Test Case 6: Division by zero
formula6 = "P / n"
context6 = {"P": 1000, "n": 0}
result6 = evaluate_formula(formula6, context6)
print(f"\nFormula: {formula6}, Context: {context6}")
print(f"Expected: None (due to error)")
print(f"Actual: {result6}")
assert result6 is None
print("\nAll tests passed if no assertion errors.")

209
src/narrative_builder.py Normal file
View File

@ -0,0 +1,209 @@
import random
from src.data_loader import get_names_data, get_text_snippets
from src.value_sampler import format_value_for_display
# Cache loaded data
NAMES_DATA = None
TEXT_SNIPPETS = None
def _get_names_data_cached():
global NAMES_DATA
if NAMES_DATA is None:
NAMES_DATA = get_names_data()
return NAMES_DATA
def _get_text_snippets_cached():
global TEXT_SNIPPETS
if TEXT_SNIPPETS is None:
TEXT_SNIPPETS = get_text_snippets()
return TEXT_SNIPPETS
def get_random_actor():
"""Generates a random actor (person or company)."""
names_data = _get_names_data_cached()
snippets = _get_text_snippets_cached()
actor_type = random.choice(["person", "company"])
actor_string_template = ""
if actor_type == "person":
actor_string_template = random.choice(snippets["actors_person"])
title = random.choice(names_data["persons_titles"])
first_name = random.choice(names_data["persons_first_names"])
last_name = random.choice(names_data["persons_last_names"])
return actor_string_template.format(person_title=title, person_first_name=first_name, person_last_name=last_name)
else: # company
actor_string_template = random.choice(snippets["actors_company"])
prefix = random.choice(names_data["companies_generic_prefix"])
# Decide if to use generic suffix or industry specific
if random.random() < 0.7: # 70% chance for generic suffix
suffix = random.choice(names_data["companies_generic_suffix"])
return actor_string_template.format(company_prefix=prefix, company_suffix=suffix, company_industry=suffix) # company_industry for templates that might use it
else:
industry_key = random.choice(list(names_data["companies_industry_specific"].keys()))
industry_suffix = random.choice(names_data["companies_industry_specific"][industry_key])
return actor_string_template.format(company_prefix=prefix, company_suffix=industry_suffix, company_industry=industry_suffix)
def build_narrative(financial_concept, known_values_data, unknown_variable_key):
"""
Constructs a problem narrative dynamically.
Args:
financial_concept (dict): The financial concept definition.
known_values_data (dict): Dict of known variable data (from value_sampler).
e.g., {"P": {'value': 1000, ...}, "i_simple_annual": {'value': 0.05, ...}}
unknown_variable_key (str): The key of the variable to be solved for (e.g., "F_simple").
Returns:
str: The generated problem narrative.
"""
snippets = _get_text_snippets_cached()
actor = get_random_actor()
narrative_parts = []
# 1. Introduction / Scenario Setup
intro_template = random.choice(snippets["scenario_introductions"])
narrative_parts.append(intro_template.format(actor=actor))
# 2. Describe the action (loan, investment) and known amounts
# This part needs to be more intelligent based on the concept and knowns/unknowns
action_type = "loan" # Default, can be refined by concept hooks
if any(hook in financial_concept.get("narrative_hooks", []) for hook in ["investment", "deposit"]):
action_type = "investment"
# Choose verb tense (past is common for setting up a problem)
if action_type == "loan":
action_verb_template_group = snippets["actions_loan_past_singular"] # Assuming singular actor for now
else: # investment
action_verb_template_group = snippets["actions_investment_past_singular"]
action_verb = random.choice(action_verb_template_group)
# Primary known value (e.g., Principal if F is unknown, or Future Value if P is unknown)
primary_value_key = ""
primary_value_desc = ""
if "P" in known_values_data and unknown_variable_key.startswith("F"): # Solving for Future Value
primary_value_key = "P"
primary_value_desc = "an initial amount of"
elif "F_simple" in known_values_data and unknown_variable_key == "P": # Solving for Principal from Simple Future
primary_value_key = "F_simple"
primary_value_desc = "a future target of"
action_verb = random.choice(snippets["actions_receive_present_singular"]) # "wants to receive"
elif "F_compound" in known_values_data and unknown_variable_key == "P": # Solving for Principal from Compound Future
primary_value_key = "F_compound"
primary_value_desc = "a future target of"
action_verb = random.choice(snippets["actions_receive_present_singular"])
# Add more conditions for other unknowns like I_simple, rates, time etc.
if primary_value_key and primary_value_key in known_values_data:
val_data = known_values_data[primary_value_key]
narrative_parts.append(f"{action_verb} {primary_value_desc} {format_value_for_display(val_data)}")
elif "P" in known_values_data: # Fallback if primary logic missed, describe P if known
narrative_parts.append(f"{action_verb} {format_value_for_display(known_values_data['P'])}")
# 3. Add purpose if applicable
if action_type == "loan" and random.random() < 0.5:
item_loan = random.choice(_get_names_data_cached()["items_loan_general"])
purpose_phrase = random.choice(snippets["purpose_phrases_loan"]).format(item_loan=item_loan)
narrative_parts.append(purpose_phrase)
elif action_type == "investment" and random.random() < 0.5:
item_investment = random.choice(_get_names_data_cached()["items_investment_general"])
purpose_phrase = random.choice(snippets["purpose_phrases_investment"]).format(item_investment=item_investment)
narrative_parts.append(purpose_phrase)
# 4. Describe other knowns (rate, time, compounding)
# Simple Interest Rate
if "i_simple_annual" in known_values_data:
rate_phrase = random.choice(snippets["rate_phrases"])
narrative_parts.append(f"{rate_phrase} {format_value_for_display(known_values_data['i_simple_annual'])}")
# Nominal Compound Interest Rate
if "r_nominal_annual" in known_values_data:
rate_phrase = random.choice(snippets["rate_phrases"])
narrative_parts.append(f"{rate_phrase} {format_value_for_display(known_values_data['r_nominal_annual'])}")
if "m_compounding_periods_per_year" in known_values_data:
freq_name = known_values_data["m_compounding_periods_per_year"]["name"]
freq_adverb = snippets["compounding_frequency_adverbs"].get(freq_name, freq_name)
comp_phrase = random.choice(snippets["compounding_phrases"]).format(compounding_frequency_adverb=freq_adverb)
narrative_parts.append(comp_phrase)
# Time (handle years, months, days - prefer years if available)
time_described = False
if "n_time_years" in known_values_data:
time_phrase = random.choice(snippets["time_phrases_duration"])
narrative_parts.append(f"{time_phrase} {format_value_for_display(known_values_data['n_time_years'])}")
time_described = True
elif "t_years" in known_values_data: # for compound interest
time_phrase = random.choice(snippets["time_phrases_duration"])
narrative_parts.append(f"{time_phrase} {format_value_for_display(known_values_data['t_years'])}")
time_described = True
# If time in months or days is primary and years not directly given (less common for this structure)
if not time_described and "n_time_months" in known_values_data:
time_phrase = random.choice(snippets["time_phrases_duration"])
narrative_parts.append(f"{time_phrase} {format_value_for_display(known_values_data['n_time_months'])}")
elif not time_described and "n_time_days" in known_values_data:
# For exact simple interest, dates are usually given instead of number of days directly
if "start_date" in known_values_data and "end_date" in known_values_data:
narrative_parts.append(f"from {format_value_for_display(known_values_data['start_date'])} to {format_value_for_display(known_values_data['end_date'])}")
else: # Fallback if only days are given
time_phrase = random.choice(snippets["time_phrases_duration"])
narrative_parts.append(f"{time_phrase} {format_value_for_display(known_values_data['n_time_days'])}")
# 5. Formulate the question about the unknown variable
question_starter = random.choice(snippets["question_starters_what_is"]) # Default
if unknown_variable_key.lower().startswith("p"):
question_starter = random.choice(snippets["question_starters_how_much"])
elif "time" in unknown_variable_key.lower():
question_starter = random.choice(snippets["question_starters_how_long"])
unknown_desc = snippets.get("variable_descriptions", {}).get(unknown_variable_key, unknown_variable_key.replace("_", " "))
question_closure_prefix = random.choice(snippets["scenario_closures_question_prefix"])
narrative_parts.append(f"{question_closure_prefix} {question_starter.lower()} {unknown_desc}?")
return " ".join(part.strip() for part in narrative_parts if part).replace(" .", ".").replace(" ?", "?") + "."
if __name__ == '__main__':
print("Testing Narrative Builder:")
# Mock data for testing (normally loaded by problem_engine)
mock_concept_simple_f = {
"concept_id": "SIMPLE_INTEREST_SOLVE_FOR_F",
"narrative_hooks": ["loan", "simple interest", "future amount"]
}
mock_known_simple_f = {
"P": {'key': 'P', 'value': 10000.0, 'currency': 'Php', 'display_precision': 2},
"i_simple_annual": {'key': 'i_simple_annual', 'value': 0.12, 'unit_display': '% per annum', 'display_precision': 2},
"n_time_years": {'key': 'n_time_years', 'value': 2.0, 'unit': 'years', 'display_precision': 1}
}
print("\n--- Test Case 1: Simple Interest, Solve for F ---")
narrative1 = build_narrative(mock_concept_simple_f, mock_known_simple_f, "F_simple")
print(narrative1)
mock_concept_compound_p = {
"concept_id": "COMPOUND_INTEREST_SOLVE_FOR_P_FROM_F",
"narrative_hooks": ["investment", "compound interest", "present value"]
}
mock_known_compound_p = {
"F_compound": {'key': 'F_compound', 'value': 25000.0, 'currency': 'Php', 'display_precision': 2},
"r_nominal_annual": {'key': 'r_nominal_annual', 'value': 0.08, 'unit_display': '%', 'display_precision': 2},
"m_compounding_periods_per_year": {'key': 'm_compounding_periods_per_year', 'name': 'quarterly', 'value': 4},
"t_years": {'key': 't_years', 'value': 5.0, 'unit': 'years', 'display_precision': 1}
}
print("\n--- Test Case 2: Compound Interest, Solve for P ---")
narrative2 = build_narrative(mock_concept_compound_p, mock_known_compound_p, "P")
print(narrative2)
# Test with a different actor type
print("\n--- Test Case 3: Different Actor ---")
narrative3 = build_narrative(mock_concept_simple_f, mock_known_simple_f, "F_simple")
print(narrative3)

345
src/problem_engine.py Normal file
View File

@ -0,0 +1,345 @@
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
import random
import math # For direct use if not going through formula_evaluator for simple conversions
from src import data_loader
from src import value_sampler
from src import formula_evaluator
from src import narrative_builder
from src import solution_presenter
from src import date_utils # For potential date-based time calculations
# --- Cached Data ---
FINANCIAL_CONCEPTS = None
TEXT_SNIPPETS_DATA = None # To avoid conflict with value_sampler's TEXT_SNIPPETS
NAMES_DATA_CACHE = None
VALUE_RANGES_CACHE = None
def _load_all_data_cached():
global FINANCIAL_CONCEPTS, TEXT_SNIPPETS_DATA, NAMES_DATA_CACHE, VALUE_RANGES_CACHE
if FINANCIAL_CONCEPTS is None:
FINANCIAL_CONCEPTS = data_loader.get_financial_concepts()
if TEXT_SNIPPETS_DATA is None:
TEXT_SNIPPETS_DATA = data_loader.get_text_snippets() # Loaded for solution_presenter and narrative_builder
if NAMES_DATA_CACHE is None:
NAMES_DATA_CACHE = data_loader.get_names_data() # Loaded for narrative_builder
if VALUE_RANGES_CACHE is None:
VALUE_RANGES_CACHE = data_loader.get_value_ranges() # Loaded for value_sampler
# Ensure sub-modules also use cached data if they have their own caches
# This is already handled by them checking their global cache variables.
# --- Variable Mapping ---
# Maps concept variable names to value_ranges.json keys
# This might need to be more dynamic or extensive based on concept variations.
CONCEPT_VAR_TO_VALUERANGE_KEY = {
"P": "principal", # Could also be loan_amount, investment_amount
"F_simple": "future_value",
"F_compound": "future_value",
"F_continuous": "future_value",
"F_maturity": "future_value", # For Banker's Discount, F is the maturity value
"F_exact_simple": "future_value",
"F_ordinary_simple": "future_value",
"I_simple": "interest_amount",
"I_exact_simple": "interest_amount",
"I_ordinary_simple": "interest_amount",
"Db_discount_amount": "interest_amount", # Or a specific "discount_amount" range
"P_proceeds": "principal", # Proceeds are a present value
"i_simple_annual": "simple_interest_rate_annual",
"i_simple_equivalent": "simple_interest_rate_annual", # Added for CONTINUOUS_COMPOUNDING_EQUIVALENT_SIMPLE_RATE
"n_time_years": "time_years", # For simple interest
"n_time_months": "time_months",
"n_time_days": "time_days",
"r_nominal_annual": "compound_interest_rate_nominal",
"t_years": "time_years", # For compound/continuous interest time
# m_compounding_periods_per_year is handled specially
"ER": "compound_interest_rate_nominal", # Effective rate is a rate
"d_discount_rate": "discount_rate_bankers",
"i_simple_equivalent": "simple_interest_rate_annual" # for comparison rates
}
def generate_problem():
"""
Generates a complete financial math problem with narrative and solution.
"""
_load_all_data_cached()
if not FINANCIAL_CONCEPTS:
return {"error": "Failed to load financial concepts."}
selected_concept = random.choice(FINANCIAL_CONCEPTS)
target_unknown = selected_concept["target_unknown"]
all_variables_data_formatted = {} # Stores full data dicts from value_sampler, keyed by concept var name
formula_context_vars = {} # Stores raw numerical values for formula evaluation, keyed by concept var name
# 1. Generate Known Values
# Convert required_knowns to a set for efficient checking and modification
required_knowns = set(selected_concept.get("required_knowns_for_target", []))
handled_vars = set()
# --- BEGIN DATE SPECIFIC LOGIC ---
# Check if the concept requires start_date and end_date
if "start_date" in required_knowns and "end_date" in required_knowns:
# Fetch date generation parameters from value_ranges.json, with defaults
date_period_config = VALUE_RANGES_CACHE.get("date_period_generation", {})
min_days_val = date_period_config.get("min_days", 30)
max_days_val = date_period_config.get("max_days", 730)
base_year_start_val = date_period_config.get("base_year_start", 1990)
base_year_end_val = date_period_config.get("base_year_end", 2030)
start_date_obj, end_date_obj, num_days_val = date_utils.get_random_date_period(
min_days=min_days_val,
max_days=max_days_val,
base_year_range=(base_year_start_val, base_year_end_val)
)
# Populate all_variables_data_formatted and formula_context_vars
all_variables_data_formatted["start_date"] = {'key': 'start_date', 'value': start_date_obj, 'unit': 'date', 'display_precision': None}
all_variables_data_formatted["end_date"] = {'key': 'end_date', 'value': end_date_obj, 'unit': 'date', 'display_precision': None}
all_variables_data_formatted["n_time_days"] = {'key': 'n_time_days', 'value': num_days_val, 'unit': 'days', 'display_precision': 0}
formula_context_vars["start_date"] = start_date_obj
formula_context_vars["end_date"] = end_date_obj
formula_context_vars["n_time_days"] = num_days_val
handled_vars.update(["start_date", "end_date", "n_time_days"]) # Mark these as handled
# Determine time_base_days and potentially time_base_year_for_exact based on financial_topic
time_base_days_val = 0
time_base_year_for_exact_val = None
if selected_concept["financial_topic"] == "Exact Simple Interest":
time_base_year_for_exact_val = start_date_obj.year
time_base_days_val = date_utils.days_in_year(time_base_year_for_exact_val)
all_variables_data_formatted["time_base_year_for_exact"] = {'key': 'time_base_year_for_exact', 'value': time_base_year_for_exact_val, 'unit': 'year', 'display_precision': 0}
formula_context_vars["time_base_year_for_exact"] = time_base_year_for_exact_val
# This variable is derived for solution steps, not typically in 'required_knowns_for_target'
elif selected_concept["financial_topic"] == "Ordinary Simple Interest":
time_base_days_val = 360
# Add time_base_days if it was determined (for Exact or Ordinary)
if time_base_days_val > 0:
all_variables_data_formatted["time_base_days"] = {'key': 'time_base_days', 'value': time_base_days_val, 'unit': 'days', 'display_precision': 0}
formula_context_vars["time_base_days"] = time_base_days_val
# This variable is derived for formulas/solution steps, not typically in 'required_knowns_for_target'
# --- END DATE SPECIFIC LOGIC ---
# Loop through all required knowns for the concept
for var_key in required_knowns: # Iterate over the original set of required knowns
if var_key in handled_vars:
continue # Skip if already handled by the date-specific logic
if var_key == "m_compounding_periods_per_year":
comp_freq_data = value_sampler.get_random_compounding_frequency()
if comp_freq_data:
all_variables_data_formatted[var_key] = comp_freq_data
formula_context_vars[var_key] = comp_freq_data["m_value"]
else:
return {"error": f"Failed to generate compounding frequency for {selected_concept['concept_id']}"}
else:
value_range_key = CONCEPT_VAR_TO_VALUERANGE_KEY.get(var_key)
if not value_range_key:
# This might be an intermediate variable that shouldn't be generated directly
# Or a variable that doesn't directly map to a single range
print(f"Warning: No value_range_key mapping for required known '{var_key}' in concept '{selected_concept['concept_id']}'. Skipping direct generation.")
continue
var_data = value_sampler.get_value_for_variable(value_range_key)
if var_data:
all_variables_data_formatted[var_key] = var_data
formula_context_vars[var_key] = var_data["value"]
else:
return {"error": f"Failed to generate value for '{var_key}' (mapped from '{value_range_key}') for concept '{selected_concept['concept_id']}'"}
# --- BEGIN PLAUSIBILITY CHECKS (e.g., for rate solving) ---
if selected_concept["concept_id"] in ["COMPOUND_INTEREST_SOLVE_FOR_RATE", "CONTINUOUS_COMPOUNDING_SOLVE_FOR_RATE", "SIMPLE_INTEREST_SOLVE_FOR_RATE_FROM_I"]:
# Ensure F > P to avoid negative or extremely low rates, unless P and F are very close.
# This logic assumes 'P' and 'F_compound'/'F_continuous'/'F_simple' are the relevant keys.
# For SIMPLE_INTEREST_SOLVE_FOR_RATE_FROM_I, it uses P and I_simple. We'd need a different check if I_simple could be negative.
p_key, f_key = None, None
if "P" in formula_context_vars:
p_key = "P"
if "F_compound" in formula_context_vars: f_key = "F_compound"
elif "F_continuous" in formula_context_vars: f_key = "F_continuous"
elif "F_simple" in formula_context_vars: f_key = "F_simple"
# For SIMPLE_INTEREST_SOLVE_FOR_RATE_FROM_I, F is not directly used, I_simple is.
# We assume I_simple will be positive. If I_simple is negative, rate will be negative.
if p_key and f_key:
max_resample_attempts = 5
attempt = 0
# We want F > P for a positive growth rate.
# If F is very close to P, rate will be very small, which is fine.
# If F < P, rate will be negative. Let's try to avoid large negative rates by ensuring F is not drastically smaller than P.
# A simple approach: if F < P, resample F until F > P * 0.8 (allowing for some negative rates but not extreme ones).
# Or, more simply for now, ensure F > P for typical positive rate problems.
while formula_context_vars[f_key] <= formula_context_vars[p_key] and attempt < max_resample_attempts:
print(f"Resampling {f_key} because {f_key} ({formula_context_vars[f_key]}) <= {p_key} ({formula_context_vars[p_key]}) for rate calculation.")
f_value_range_key = CONCEPT_VAR_TO_VALUERANGE_KEY.get(f_key)
if f_value_range_key:
var_data_f = value_sampler.get_value_for_variable(f_value_range_key)
if var_data_f:
all_variables_data_formatted[f_key] = var_data_f
formula_context_vars[f_key] = var_data_f["value"]
else: # Failed to resample F, break to avoid infinite loop
break
else: # No range key for F, break
break
attempt += 1
if formula_context_vars[f_key] <= formula_context_vars[p_key]:
print(f"Warning: Could not ensure {f_key} > {p_key} after {max_resample_attempts} attempts for concept {selected_concept['concept_id']}. Proceeding with current values.")
# --- END PLAUSIBILITY CHECKS ---
# 2. Handle Time Conversions (Example for Simple Interest if n_time_years is needed but months/days are used)
# This logic can be expanded. For now, assume n_time_years is directly generated if listed as required.
# If a concept *requires* n_time_years, but we want to sometimes *start* from months or days:
if selected_concept["financial_topic"] == "Simple Interest":
if "n_time_years" in formula_context_vars and random.random() < 0.3: # 30% chance to convert from months
original_months_data = value_sampler.get_value_for_variable("time_months")
if original_months_data:
all_variables_data_formatted["n_time_months"] = original_months_data
# Override n_time_years with converted value
formula_context_vars["n_time_years"] = original_months_data["value"] / 12.0
# Update the n_time_years entry in all_variables_data_formatted as well
all_variables_data_formatted["n_time_years"] = {
'key': 'time_years', # original key from value_ranges
'value': formula_context_vars["n_time_years"],
'unit': 'years',
'display_precision': all_variables_data_formatted["n_time_years"].get('display_precision', 4) # keep original precision setting
}
# Could add similar logic for days -> years conversion here, possibly using date_utils for exact days.
# 3. Calculate Intermediate Variables defined in the concept's formulas
# These are formulas NOT for the target_unknown.
# Ensure they are calculated in a sensible order if there are dependencies.
# For now, assume formulas are simple enough or ordered correctly in JSON.
# A more robust way would be to build a dependency graph.
# Create a list of formulas to evaluate, target last
formulas_to_eval = []
if isinstance(selected_concept["formulas"], dict):
for var, formula_str in selected_concept["formulas"].items():
if var != target_unknown:
formulas_to_eval.append((var, formula_str))
if target_unknown in selected_concept["formulas"]: # Add target formula last
formulas_to_eval.append((target_unknown, selected_concept["formulas"][target_unknown]))
else: # Should not happen based on current JSON structure
return {"error": f"Formulas for concept {selected_concept['concept_id']} are not in expected dict format."}
calculated_solution_value = None
calculated_solution_data_formatted = None
for var_to_calc, formula_str in formulas_to_eval:
calc_value = formula_evaluator.evaluate_formula(formula_str, formula_context_vars)
if calc_value is None:
return {"error": f"Failed to evaluate formula for '{var_to_calc}' in concept '{selected_concept['concept_id']}'. Context: {formula_context_vars}"}
formula_context_vars[var_to_calc] = calc_value # Add to context for subsequent formulas
# Create formatted data for this calculated variable (for narrative/solution)
# Need to determine its type (currency, rate, time, etc.) for proper formatting.
# We can infer from CONCEPT_VAR_TO_VALUERANGE_KEY or add 'type' to financial_concepts.json vars.
value_range_key_for_calc_var = CONCEPT_VAR_TO_VALUERANGE_KEY.get(var_to_calc)
base_config = VALUE_RANGES_CACHE.get(value_range_key_for_calc_var, {}) if VALUE_RANGES_CACHE else {}
formatted_data_for_calc_var = {
'key': var_to_calc, # Use the concept's variable name
'value': calc_value,
'currency': base_config.get('currency'),
'unit': base_config.get('unit'),
'unit_display': base_config.get('unit_display'),
'display_precision': base_config.get('display_precision', base_config.get('decimals'))
}
# If it's a rate, ensure unit_display is set correctly
if "rate" in var_to_calc.lower() and not formatted_data_for_calc_var.get('unit_display'):
if value_range_key_for_calc_var and VALUE_RANGES_CACHE and value_range_key_for_calc_var in VALUE_RANGES_CACHE:
formatted_data_for_calc_var['unit_display'] = VALUE_RANGES_CACHE[value_range_key_for_calc_var].get('unit_display', '%') # Default to %
else: # Fallback if no specific range key
formatted_data_for_calc_var['unit_display'] = '%' if var_to_calc != "n_total_compounding_periods" else "periods"
all_variables_data_formatted[var_to_calc] = formatted_data_for_calc_var
if var_to_calc == target_unknown:
calculated_solution_value = calc_value
calculated_solution_data_formatted = formatted_data_for_calc_var
if calculated_solution_value is None:
return {"error": f"Target unknown '{target_unknown}' was not calculated for concept '{selected_concept['concept_id']}'."}
# 4. Build Narrative
problem_narrative = narrative_builder.build_narrative(selected_concept, all_variables_data_formatted, target_unknown)
# 5. Generate Guided Solution
solution_steps = solution_presenter.generate_guided_solution(selected_concept, all_variables_data_formatted, calculated_solution_data_formatted)
return {
"concept_id": selected_concept["concept_id"],
"topic": selected_concept["financial_topic"],
"problem_statement": problem_narrative,
"target_unknown_key": target_unknown,
"known_values_data": {k: v for k, v in all_variables_data_formatted.items() if k in selected_concept.get("required_knowns_for_target", []) or k in ["m_compounding_periods_per_year", "n_time_months", "start_date", "end_date"]}, # Show originally generated knowns
"all_variables_for_solution": all_variables_data_formatted, # Includes intermediates
"calculated_answer_raw": calculated_solution_value,
"calculated_answer_formatted": value_sampler.format_value_for_display(calculated_solution_data_formatted),
"solution_steps": solution_steps
}
if __name__ == '__main__':
print("Generating a sample financial problem...\n")
# Ensure data is loaded for sub-modules if they cache independently on first call
value_sampler._get_value_ranges_cached()
narrative_builder._get_names_data_cached()
narrative_builder._get_text_snippets_cached()
solution_presenter._get_text_snippets_cached()
# Load all concepts
all_concepts = data_loader.get_financial_concepts()
if not all_concepts:
print("Failed to load financial concepts for testing. Exiting.")
sys.exit(1)
print(f"Found {len(all_concepts)} concepts to test.\n")
original_financial_concepts_global = FINANCIAL_CONCEPTS # Save the global state if it was set
for i, concept_to_test in enumerate(all_concepts):
# Temporarily set the global FINANCIAL_CONCEPTS to only the current concept for generate_problem()
# This ensures generate_problem picks this specific concept.
FINANCIAL_CONCEPTS = [concept_to_test]
print(f"\n--- Testing Concept {i+1}/{len(all_concepts)}: {concept_to_test['concept_id']} ---")
problem = generate_problem() # generate_problem will use the modified global FINANCIAL_CONCEPTS
if "error" in problem:
print(f"Error generating problem: {problem['error']}")
else:
print(f"Topic: {problem['topic']}")
print("\nProblem Statement:")
print(problem['problem_statement'])
# Ensure TEXT_SNIPPETS_DATA is loaded for the question part
if TEXT_SNIPPETS_DATA is None: # Should have been loaded by _load_all_data_cached
_load_all_data_cached()
print(f"\nQuestion: What is the {TEXT_SNIPPETS_DATA['variable_descriptions'].get(problem['target_unknown_key'], problem['target_unknown_key'])}?")
print("\nGuided Solution:")
for step in problem['solution_steps']:
print(step)
print(f"\nFinal Answer ({problem['target_unknown_key']}): {problem['calculated_answer_formatted']}")
print("---------------------------------------\n")
FINANCIAL_CONCEPTS = original_financial_concepts_global # Restore original global state
print("Completed testing all concepts.")

345
src/solution_presenter.py Normal file
View File

@ -0,0 +1,345 @@
import random
import re # Added for regex substitution
import decimal # Added for precise formatting of numbers in formulas
from src.data_loader import get_text_snippets
from src.value_sampler import format_value_for_display
# Cache loaded data
TEXT_SNIPPETS = None
def _get_text_snippets_cached():
global TEXT_SNIPPETS
if TEXT_SNIPPETS is None:
TEXT_SNIPPETS = get_text_snippets()
return TEXT_SNIPPETS
def get_snippet(key_path):
"""Retrieves a snippet from text_snippets.json using a dot-separated path."""
snippets = _get_text_snippets_cached()
keys = key_path.split('.')
current_level = snippets
for key in keys:
if isinstance(current_level, dict) and key in current_level:
current_level = current_level[key]
else:
return f"{{Error: Snippet not found for {key_path}}}"
return current_level
def _format_value_for_formula(raw_value, var_data):
"""
Formats a raw numerical value for substitution into a formula string.
Rates are kept as decimals. Uses a higher precision for rates,
otherwise display_precision from var_data.
"""
unit_display_str = var_data.get("unit_display")
unit_str = var_data.get("unit")
is_rate = (isinstance(unit_display_str, str) and "%" in unit_display_str) or \
(isinstance(unit_str, str) and "%" in unit_str)
if isinstance(raw_value, (float, decimal.Decimal)):
if is_rate:
# Use a higher, fixed precision for rates in formulas
# to maintain accuracy for manual calculation from the substituted formula.
# The internal_precision from value_ranges.json (e.g., 8) is for generation.
# For substitution display, 4 to 6 decimal places for the rate decimal is usually sufficient.
# Let's use 4 as a balance, as rates are often quoted to 2 decimal places as percentages.
# So, 0.123456 (12.3456%) would be 0.1235 if display_precision for % is 2,
# but for formula, we want 0.123456 or similar.
# Using 4 for the decimal: 0.0737 -> 0.0737, 0.1673 -> 0.1673
# Let's use 6 to be safer and show more of the internal value.
return f"{raw_value:.6f}"
else:
precision = var_data.get('display_precision')
if precision is not None:
return f"{raw_value:.{precision}f}"
else: # Default precision for non-rate floats if not specified
return f"{raw_value:.6f}"
elif isinstance(raw_value, int):
return str(raw_value)
else: # Fallback for other types (e.g. already a string, though less likely here)
return str(raw_value)
def generate_guided_solution(financial_concept, all_variables_data, calculated_solution_data):
"""
Generates a step-by-step guided solution.
"""
solution_steps_text = []
step_counter = 1
target_unknown_key = financial_concept["target_unknown"]
symbolic_formula_rhs = financial_concept["formulas"].get(target_unknown_key, "Formula not defined")
for step_key_path in financial_concept.get("solution_step_keys", []):
step_text_template = get_snippet(step_key_path)
formatted_step_text = f"{step_counter}. {step_text_template}"
if step_key_path == "solution_guidance.identify_knowns":
knowns_list_text = [f"{step_counter}. {step_text_template}"]
for var_key in financial_concept.get("required_knowns_for_target", []):
if var_key in all_variables_data:
var_data = all_variables_data[var_key]
var_desc = get_snippet(f"variable_descriptions.{var_key}")
knowns_list_text.append(f" - {var_desc} ({var_key}): {format_value_for_display(var_data)}")
if "m_compounding_periods_per_year" in all_variables_data and "m_compounding_periods_per_year" not in financial_concept.get("required_knowns_for_target", []):
var_data = all_variables_data["m_compounding_periods_per_year"]
var_desc = get_snippet(f"variable_descriptions.m_compounding_periods_per_year")
knowns_list_text.append(f" - {var_desc} (m): {var_data['name']} (m={var_data['value']})")
solution_steps_text.extend(knowns_list_text)
step_counter += 1
continue
elif step_key_path == "solution_guidance.state_formula":
formula_rhs_to_display = symbolic_formula_rhs
# Special handling for complex formulas to show a more user-friendly version
if target_unknown_key == "i_rate_per_period" and "COMPOUND_INTEREST_SOLVE_FOR_RATE" in financial_concept["concept_id"]:
formula_rhs_to_display = "(F/P)^(1/n) - 1" # r = i * m is handled as a subsequent step if needed
elif target_unknown_key == "n_total_compounding_periods" and "COMPOUND_INTEREST_SOLVE_FOR_TIME" in financial_concept["concept_id"]:
formula_rhs_to_display = "log(F/P) / log(1+i)" # t = n / m is handled as a subsequent step
formatted_step_text = formatted_step_text.format(
target_variable_lhs=target_unknown_key,
formula_symbolic_rhs=formula_rhs_to_display
)
elif step_key_path == "solution_guidance.substitute_values":
formula_with_values_rhs = symbolic_formula_rhs
# Iterate through all variables that might be in the formula
# Sort by length of key, descending, to replace longer keys first (e.g. "n_time_days" before "n")
sorted_var_keys = sorted(all_variables_data.keys(), key=len, reverse=True)
for var_key in sorted_var_keys:
if var_key in formula_with_values_rhs: # Check if key is part of formula string
var_data = all_variables_data[var_key]
# Format the raw value for formula substitution (decimal for rates, specific precision)
value_for_formula = _format_value_for_formula(var_data['value'], var_data)
# Use regex to replace whole words only
formula_with_values_rhs = re.sub(r'\b' + re.escape(var_key) + r'\b', value_for_formula, formula_with_values_rhs)
formatted_step_text = formatted_step_text.format(
target_variable_lhs=target_unknown_key,
formula_with_values_rhs=formula_with_values_rhs
)
elif step_key_path == "solution_guidance.perform_calculation":
pass
elif step_key_path == "solution_guidance.final_answer_is":
unknown_desc = get_snippet(f"variable_descriptions.{target_unknown_key}")
formatted_step_text = formatted_step_text.replace("{unknown_variable_description}", unknown_desc)
formatted_step_text += f" {format_value_for_display(calculated_solution_data)}"
elif step_key_path == "solution_guidance.convert_time_to_years":
original_time_var = None
original_time_unit = ""
if "n_time_months" in all_variables_data and "n_time_years" in all_variables_data:
original_time_var = all_variables_data["n_time_months"]
original_time_unit = "months"
elif "n_time_days" in all_variables_data and "n_time_years" in all_variables_data and \
"start_date" not in all_variables_data:
original_time_var = all_variables_data["n_time_days"]
original_time_unit = "days"
if original_time_var and "n_time_years" in all_variables_data:
formatted_step_text = formatted_step_text.format(
original_time_value=_format_value_for_formula(original_time_var['value'], original_time_var),
original_time_unit=original_time_unit,
converted_time_value_years=_format_value_for_formula(all_variables_data['n_time_years']['value'], all_variables_data['n_time_years'])
)
else:
if (financial_concept["financial_topic"] == "Simple Interest" and \
"n_time_years" in financial_concept.get("required_knowns_for_target", [])) or \
(financial_concept["financial_topic"] in ["Exact Simple Interest", "Ordinary Simple Interest"]):
continue
formatted_step_text = f"{step_counter}. (Time conversion step - data missing or not applicable)"
elif step_key_path == "solution_guidance.calculate_interest_rate_per_period":
if "r_nominal_annual" in all_variables_data and \
"m_compounding_periods_per_year" in all_variables_data and \
"i_rate_per_period" in all_variables_data:
formatted_step_text = formatted_step_text.format(
nominal_rate_decimal=_format_value_for_formula(all_variables_data['r_nominal_annual']['value'], all_variables_data['r_nominal_annual']),
compounding_periods_per_year=all_variables_data['m_compounding_periods_per_year']['value'],
interest_rate_per_period_decimal=_format_value_for_formula(all_variables_data['i_rate_per_period']['value'], all_variables_data['i_rate_per_period'])
)
else: continue
elif step_key_path == "solution_guidance.calculate_total_periods":
if "t_years" in all_variables_data and \
"m_compounding_periods_per_year" in all_variables_data and \
"n_total_compounding_periods" in all_variables_data:
formatted_step_text = formatted_step_text.format(
time_in_years=_format_value_for_formula(all_variables_data['t_years']['value'], all_variables_data['t_years']),
compounding_periods_per_year=all_variables_data['m_compounding_periods_per_year']['value'],
total_periods=_format_value_for_formula(all_variables_data['n_total_compounding_periods']['value'], all_variables_data['n_total_compounding_periods'])
)
else: continue
elif step_key_path == "solution_guidance.days_in_period":
if "start_date" in all_variables_data and \
"end_date" in all_variables_data and \
"n_time_days" in all_variables_data:
formatted_step_text = formatted_step_text.format(
start_date=format_value_for_display(all_variables_data['start_date']),
end_date=format_value_for_display(all_variables_data['end_date']),
number_of_days=all_variables_data['n_time_days']['value']
)
else: continue
elif step_key_path == "solution_guidance.check_leap_year":
year_to_check_data = all_variables_data.get("time_base_year_for_exact")
if not year_to_check_data and "start_date" in all_variables_data:
year_to_check_data = {'value': all_variables_data["start_date"]['value'].year}
if year_to_check_data:
from src.date_utils import is_leap_year
year_val = year_to_check_data['value']
is_leap = is_leap_year(year_val)
formatted_step_text = formatted_step_text.format(
year=year_val,
is_or_is_not="is" if is_leap else "is not"
)
else: continue
elif step_key_path == "solution_guidance.determine_time_base_exact":
year_for_base_data = all_variables_data.get("time_base_year_for_exact")
days_in_year_data = all_variables_data.get("time_base_days")
if year_for_base_data and days_in_year_data:
formatted_step_text = formatted_step_text.format(
year=year_for_base_data['value'],
days_in_year=days_in_year_data['value']
)
else: continue
elif step_key_path == "solution_guidance.determine_time_base_ordinary":
if "time_base_days" in all_variables_data and all_variables_data["time_base_days"]['value'] == 360:
pass
else: continue
elif step_key_path == "solution_guidance.calculate_n_time_years_fractional":
if "n_time_days" in all_variables_data and \
"time_base_days" in all_variables_data and \
"n_time_years_fractional" in all_variables_data:
formatted_step_text = formatted_step_text.format(
n_time_days=all_variables_data['n_time_days']['value'],
time_base_days=all_variables_data['time_base_days']['value'],
n_time_years_fractional_value=_format_value_for_formula(all_variables_data['n_time_years_fractional']['value'], all_variables_data['n_time_years_fractional'])
)
else: continue
elif step_key_path == "solution_guidance.intermediate_step":
# This needs to be more robust or specific step keys should be used.
intermediate_var_name = None
intermediate_var_data = None
if "Db_discount_amount" in all_variables_data and "BANKERS_DISCOUNT" in financial_concept["concept_id"]:
intermediate_var_name = "Db_discount_amount"
elif "i_rate_per_period" in all_variables_data and "COMPOUND_INTEREST_SOLVE_FOR_RATE" in financial_concept["concept_id"] and target_unknown_key == "r_nominal_annual":
intermediate_var_name = "i_rate_per_period"
elif "n_total_compounding_periods" in all_variables_data and "COMPOUND_INTEREST_SOLVE_FOR_TIME" in financial_concept["concept_id"] and target_unknown_key == "t_years":
intermediate_var_name = "n_total_compounding_periods"
if intermediate_var_name and intermediate_var_name in all_variables_data:
intermediate_var_data = all_variables_data[intermediate_var_name]
intermediate_var_desc = get_snippet(f"variable_descriptions.{intermediate_var_name}")
# For i_rate_per_period, we want to show more precision and not as percent here
if intermediate_var_name == "i_rate_per_period":
display_val = _format_value_for_formula(intermediate_var_data['value'], intermediate_var_data)
elif intermediate_var_name == "n_total_compounding_periods":
display_val = f"{_format_value_for_formula(intermediate_var_data['value'], intermediate_var_data)} periods"
else: # General case, use standard display formatting
display_val = format_value_for_display(intermediate_var_data)
formatted_step_text = formatted_step_text.format(step_name=intermediate_var_desc) + f" {display_val}"
else:
# Fallback or skip if no clear intermediate variable is identified for this generic step
formatted_step_text = f"{step_counter}. (Intermediate calculation step)"
solution_steps_text.append(formatted_step_text)
step_counter += 1
return solution_steps_text
if __name__ == '__main__':
print("Testing Solution Presenter:")
from datetime import date
# from src.value_sampler import format_value_for_display # Already imported at top level
# Mock data for testing
mock_snippets_data_for_test = { # Renamed to avoid conflict if this file is imported elsewhere
"solution_guidance": {
"identify_knowns": "First, identify knowns:",
"state_formula": "The relevant formula for this problem is: {target_variable_lhs} = {formula_symbolic_rhs}",
"substitute_values": "Now, we substitute the known values: {target_variable_lhs} = {formula_with_values_rhs}",
"perform_calculation": "Performing calculation...",
"final_answer_is": "Therefore, the {unknown_variable_description} is:",
"convert_time_to_years": "Convert time: {original_time_value} {original_time_unit} = {converted_time_value_years} years.",
"calculate_interest_rate_per_period": "Calculate the interest rate per compounding period (i): i = r / m = {nominal_rate_decimal} / {compounding_periods_per_year} = {interest_rate_per_period_decimal}.",
"calculate_total_periods": "Calculate the total number of compounding periods (n): n = t * m = {time_in_years} years * {compounding_periods_per_year} = {total_periods} periods.",
"intermediate_step": "The intermediate result for {step_name} is:",
"days_in_period": "The number of days from {start_date} to {end_date} is {number_of_days} days.",
"check_leap_year": "{year} is {is_or_is_not} a leap year.",
"determine_time_base_exact": "For exact simple interest, the time base is the actual number of days in the reference year ({year}), which is {days_in_year} days.",
"determine_time_base_ordinary": "For ordinary simple interest, the time base is 360 days.",
"calculate_n_time_years_fractional": "Calculate the time as a fraction of a year (t_fractional): t_fractional = number of days / time base = {n_time_days} / {time_base_days} = {n_time_years_fractional_value}."
},
"variable_descriptions": {
"P": "Principal", "F_simple": "Future Value (Simple)", "i_simple_annual": "Annual Simple Interest Rate",
"n_time_years": "Time in Years", "n_time_months": "Time in Months", "n_time_days": "Time in Days",
"F_compound": "Future Value (Compound)", "r_nominal_annual": "Nominal Annual Rate",
"m_compounding_periods_per_year": "Compounding Frequency", "t_years": "Time in Years (Compound)",
"i_rate_per_period": "interest rate per compounding period",
"n_total_compounding_periods": "total number of compounding periods",
"Db_discount_amount": "Banker's Discount Amount",
"start_date": "Start Date", "end_date": "End Date", "time_base_days": "Time Base (Days)",
"n_time_years_fractional": "Time (Fractional Years)",
"I_exact_simple": "Exact Simple Interest", "F_exact_simple": "Future Value (Exact Simple Interest)",
"I_ordinary_simple": "Ordinary Simple Interest", "F_ordinary_simple": "Future Value (Ordinary Simple Interest)"
}
}
original_get_snippets = _get_text_snippets_cached # Save original
_get_text_snippets_cached = lambda: mock_snippets_data_for_test # Override for this test block
# Test Case 4: Exact Simple Interest, Solve for I
concept4 = {
"concept_id": "EXACT_SIMPLE_INTEREST_SOLVE_FOR_I",
"financial_topic": "Exact Simple Interest",
"target_unknown": "I_exact_simple",
"formulas": {
"n_time_years_fractional": "n_time_days / time_base_days", # Intermediate
"I_exact_simple": "P * i_simple_annual * n_time_years_fractional" # Target
},
"required_knowns_for_target": ["P", "i_simple_annual", "start_date", "end_date"],
# Note: n_time_days, time_base_days, time_base_year_for_exact are derived by problem_engine
# n_time_years_fractional is an intermediate calculation based on the above formula
"solution_step_keys": [
"solution_guidance.identify_knowns",
"solution_guidance.days_in_period",
"solution_guidance.check_leap_year",
"solution_guidance.determine_time_base_exact",
"solution_guidance.calculate_n_time_years_fractional", # This step calculates n_time_years_fractional
"solution_guidance.state_formula", # Shows I_exact_simple = P * i_simple_annual * n_time_years_fractional
"solution_guidance.substitute_values", # Shows I_exact_simple = 5000.00 * 0.05 * 0.495890
"solution_guidance.perform_calculation",
"solution_guidance.final_answer_is"
]
}
all_vars4 = {
"P": {'key': 'P', 'value': 5000.0, 'currency': 'Php', 'display_precision': 2},
"i_simple_annual": {'key': 'i_simple_annual', 'value': 0.05, 'unit_display': '% p.a.', 'display_precision': 4}, # Rate, use 4 for decimal 0.0500
"start_date": {'key': 'start_date', 'value': date(2023, 1, 15), 'unit': 'date'},
"end_date": {'key': 'end_date', 'value': date(2023, 7, 15), 'unit': 'date'},
"n_time_days": {'key': 'n_time_days', 'value': 181, 'unit': 'days', 'display_precision': 0},
"time_base_year_for_exact": {'key': 'time_base_year_for_exact', 'value': 2023, 'display_precision': 0},
"time_base_days": {'key': 'time_base_days', 'value': 365, 'unit': 'days', 'display_precision': 0},
"n_time_years_fractional": {'key': 'n_time_years_fractional', 'value': 181/365, 'display_precision': 6}, # This is calculated
"I_exact_simple": {'key': 'I_exact_simple', 'value': 5000 * 0.05 * (181/365), 'currency': 'Php', 'display_precision': 2} # Final answer
}
solution4_text = generate_guided_solution(concept4, all_vars4, all_vars4["I_exact_simple"])
print("\n--- Test Case 4: Exact Simple Interest (I) ---")
for step in solution4_text: print(step)
_get_text_snippets_cached = original_get_snippets # Restore original

179
src/value_sampler.py Normal file
View File

@ -0,0 +1,179 @@
import random
import decimal
import datetime # Added for type checking
from src.data_loader import get_value_ranges
from src import date_utils # Added for date formatting
# Cache loaded data to avoid repeated file I/O
VALUE_RANGES = None
def _get_value_ranges_cached():
"""Returns cached value_ranges data, loading if not already cached."""
global VALUE_RANGES
if VALUE_RANGES is None:
VALUE_RANGES = get_value_ranges()
return VALUE_RANGES
def get_random_float(min_val, max_val, precision=None):
"""Generates a random float between min_val and max_val with specified precision."""
val = random.uniform(min_val, max_val)
if precision is not None:
# Using Decimal for precise rounding
return float(decimal.Decimal(str(val)).quantize(decimal.Decimal('1e-' + str(precision)), rounding=decimal.ROUND_HALF_UP))
return val
def get_random_int(min_val, max_val):
"""Generates a random integer between min_val and max_val (inclusive)."""
return random.randint(min_val, max_val)
def get_value_for_variable(variable_key):
"""
Generates a random value for a given variable key based on value_ranges.json.
Returns a dictionary containing the value and its unit/currency if applicable.
Example: {'value': 15000.50, 'currency': 'Php', 'display_str': 'Php 15,000.50'}
{'value': 0.12, 'unit_display': '% per annum', 'display_str': '12.00% per annum'}
{'value': 5, 'unit': 'years', 'display_str': '5 years'}
"""
ranges = _get_value_ranges_cached()
if not ranges or variable_key not in ranges:
print(f"Error: No range definition found for variable key '{variable_key}' in value_ranges.json")
return None
config = ranges[variable_key]
val = None
result = {'key': variable_key}
if config.get("integer", False):
val = get_random_int(config["min"], config["max"])
result['value'] = val
else:
# For floats, use internal_precision for generation if available, otherwise default to a reasonable precision
# Display precision will be handled separately or by a formatting function
internal_precision = config.get("internal_precision", config.get("decimals", 4)) # Default to 4 if no precision specified
val = get_random_float(config["min"], config["max"], internal_precision)
result['value'] = val
if "currency" in config:
result["currency"] = config["currency"]
if "unit" in config:
result["unit"] = config["unit"]
if "unit_display" in config: # For rates like "% per annum"
result["unit_display"] = config["unit_display"]
# Store precision details for later formatting
result["display_precision"] = config.get("display_precision", config.get("decimals")) # Decimals is often used for currency
return result
def get_random_compounding_frequency():
"""Selects a random compounding frequency and its 'm' value."""
ranges = _get_value_ranges_cached()
if not ranges or "compounding_frequency_options" not in ranges:
print("Error: 'compounding_frequency_options' not found in value_ranges.json")
return None, None
options = ranges["compounding_frequency_options"]
frequency_name = random.choice(list(options.keys()))
m_value = options[frequency_name]
return {"name": frequency_name, "m_value": m_value, "key": "m_compounding_periods_per_year", "value": m_value}
def format_value_for_display(value_data):
"""
Formats a value generated by get_value_for_variable for display.
Example input: {'key': 'principal', 'value': 15000.5, 'currency': 'Php', 'display_precision': 2}
Example output: "Php 15,000.50"
Example input: {'key': 'simple_interest_rate_annual', 'value': 0.12, 'unit_display': '% per annum', 'display_precision': 2}
Example output: "12.00% per annum"
"""
# --- BEGIN DEBUG PRINT ---
# print(f"DEBUG: format_value_for_display received value_data: {value_data}")
# --- END DEBUG PRINT ---
if not isinstance(value_data, dict) or 'value' not in value_data:
# print(f"DEBUG: Fallback str(value_data): {str(value_data)}")
return str(value_data) # Fallback
val = value_data['value']
display_precision = value_data.get('display_precision')
# --- BEGIN DEBUG PRINT ---
# print(f"DEBUG: val: {val}, display_precision: {display_precision}, type(val): {type(val)}")
# --- END DEBUG PRINT ---
# Handle date objects first
if isinstance(val, datetime.date):
# print(f"DEBUG: Path: Date. Result: {date_utils.format_date_for_display(val)}")
return date_utils.format_date_for_display(val)
# Format number with precision
if display_precision is not None and isinstance(val, (float, decimal.Decimal)):
# Check if it's a rate to be displayed as percentage
if value_data.get("unit_display") and "%" in value_data["unit_display"]: # Check if unit_display exists and is not None
formatted_num = f"{val * 100:.{display_precision}f}"
else:
formatted_num = f"{val:.{display_precision}f}"
# Add thousands separator for currency-like values
if "currency" in value_data and val >= 1000:
parts = formatted_num.split('.')
parts[0] = "{:,}".format(int(parts[0].replace(',', ''))) # Apply to integer part
formatted_num = ".".join(parts)
elif isinstance(val, int):
formatted_num = str(val)
if "currency" in value_data and val >= 1000:
formatted_num = "{:,}".format(val)
else:
formatted_num = str(val)
if "currency" in value_data and value_data["currency"] is not None: # Ensure currency is not None
# print(f"DEBUG: Path: Currency. Result: {value_data['currency']} {formatted_num}")
return f"{value_data['currency']} {formatted_num}"
elif "unit_display" in value_data and value_data["unit_display"] is not None: # Ensure unit_display is not None
# print(f"DEBUG: Path: Unit Display. Result: {formatted_num}{value_data['unit_display']}")
return f"{formatted_num}{value_data['unit_display']}"
elif "unit" in value_data and value_data["unit"] is not None: # Ensure unit is not None
# print(f"DEBUG: Path: Unit. Result: {formatted_num} {value_data['unit']}")
return f"{formatted_num} {value_data['unit']}"
else:
# print(f"DEBUG: Path: Formatted Num Only. Result: {formatted_num}")
return formatted_num
if __name__ == '__main__':
print("Testing Value Sampler:")
# Test get_value_for_variable
print("\n--- Testing get_value_for_variable ---")
principal_data = get_value_for_variable("principal")
if principal_data:
print(f"Generated Principal Data: {principal_data}")
print(f"Formatted Principal: {format_value_for_display(principal_data)}")
rate_data = get_value_for_variable("simple_interest_rate_annual")
if rate_data:
print(f"Generated Rate Data: {rate_data}")
print(f"Formatted Rate: {format_value_for_display(rate_data)}")
time_data = get_value_for_variable("time_years")
if time_data:
print(f"Generated Time Data: {time_data}")
print(f"Formatted Time: {format_value_for_display(time_data)}")
# Test get_random_compounding_frequency
print("\n--- Testing get_random_compounding_frequency ---")
comp_freq = get_random_compounding_frequency()
if comp_freq:
print(f"Generated Compounding Frequency: {comp_freq}")
# Test edge cases or specific formatting
print("\n--- Testing Specific Formatting ---")
test_currency_large = {'key': 'principal', 'value': 1234567.89, 'currency': 'Php', 'display_precision': 2}
print(f"Large Currency: {format_value_for_display(test_currency_large)}")
test_currency_small = {'key': 'principal', 'value': 123.45, 'currency': 'Php', 'display_precision': 2}
print(f"Small Currency: {format_value_for_display(test_currency_small)}")
test_rate_percent = {'key': 'simple_interest_rate_annual', 'value': 0.085, 'unit_display': '% per annum', 'display_precision': 2}
print(f"Rate as Percent: {format_value_for_display(test_rate_percent)}")
test_integer_unit = {'key': 'time_years', 'value': 5, 'unit': 'years'}
print(f"Integer with Unit: {format_value_for_display(test_integer_unit)}")

8
uv.lock generated Normal file
View File

@ -0,0 +1,8 @@
version = 1
revision = 2
requires-python = ">=3.9"
[[package]]
name = "problem-generator"
version = "0.1.0"
source = { virtual = "." }