diff --git a/rag/server.py b/rag/server.py index aa25ff4e4..bb175c9d3 100644 --- a/rag/server.py +++ b/rag/server.py @@ -8,7 +8,8 @@ from langchain_ollama import OllamaEmbeddings from langchain.chat_models import init_chat_model from dotenv import load_dotenv -from typing import List +from typing import List, Dict, Any +from triangle import triangle_build, dev_select, tail_constant, tail_curve, ibnr_estimate # Set up logging to stderr to avoid interfering with JSON-RPC over stdout logging.basicConfig( @@ -68,6 +69,7 @@ def generate_answer(question: str, context_docs: List[Document]) -> str: response = llm.invoke(messages) return response.content + @mcp.tool() def search_friedland_paper(prompt: str) -> str: """Search the Friedland actuarial paper for information""" @@ -88,5 +90,125 @@ def search_both_papers(prompt: str) -> str: all_docs = friedland_docs + werner_modlin_docs return generate_answer(prompt, all_docs) +@mcp.tool() +def triangle_build_tool(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Build an actuarial triangle from input data using chainladder-python. + + Required Input: { + rows: [{origin, dev, value, segment?}], + value_type: "cumulative"|"incremental", + metric: "PaidLoss"|"ReportedLoss"|..., + } + + Optional Input: { + exposure: [15000, 18000, 20000], # Exposure per origin period + origin_col: "origin", # Name of origin column in rows (default: 'origin') + dev_col: "dev", # Name of development column in rows (default: 'dev') + value_col: "value" # Name of value column in rows (default: 'value') + } + + Output: { + triangle_id, + profile: {n_origin, n_dev, has_exposure}, + warnings: [] + } + """ + return triangle_build(input_data) + +@mcp.tool() +def dev_select_tool(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Perform development factor selection using chainladder. + + Input: { + triangle_id: "uuid-string", + averaging: "volume"|"simple"|"median", + age_exclusions: [], + min_obs: 1, + tail_spec: null + } + + Output: { + age_to_age: [], + LDF: [], + CDF: [], + tail_factor: 1.0, + diagnostics: {} + } + """ + return dev_select(input_data) + +@mcp.tool() +def tail_constant_tool(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Apply constant tail factor to triangle. + + Input: { + triangle_id: "uuid-string", + tail_factor: 1.05 + } + + Output: { + tail_factor: 1.05, + ldf: [], + cdf: [], + diagnostics: {} + } + """ + return tail_constant(input_data) + +@mcp.tool() +def tail_curve_tool(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Apply curve-fitted tail to triangle. + + Input: { + triangle_id: "uuid-string", + extrap_periods: 100, + fit_period: null + } + + Output: { + tail_factor: 1.03, + ldf: [], + cdf: [], + diagnostics: {} + } + """ + return tail_curve(input_data) + +@mcp.tool() +def ibnr_estimate_tool(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Calculate IBNR and Ultimate using various actuarial methods. + + Input: { + triangle_id: "uuid-string", + method: "chainladder"|"bornhuetter_ferguson"|"benktander"|"expected_losses", + apriori: 0.75, # Required for bornhuetter_ferguson, benktander, expected_losses + n_iters: 1, + trend: 0.0 + } + + Note: + - chainladder method only requires triangle_id + - Other methods require exposure data in triangle_build + + Output: { + ultimate: [], + ibnr: [], + latest_diagonal: [], + diagnostics: {} + } + + TODO: Implement additional methods: + - case_outstanding: Requires incurred vs paid triangle structure + - cape_cod: Needs specific exposure data formatting + - berquist_sherman: Requires additional parameter configuration + - frequency_severity: Custom implementation needed for the 3 variations(not in chainladder) + """ + return ibnr_estimate(input_data) + if __name__ == "__main__": mcp.run() \ No newline at end of file diff --git a/rag/triangle.py b/rag/triangle.py new file mode 100644 index 000000000..3319f2e38 --- /dev/null +++ b/rag/triangle.py @@ -0,0 +1,325 @@ +""" +Minimal triangle functionality - just convert input data to chainladder Triangle. +""" + +import uuid +import pandas as pd +from typing import Dict, Any +import logging + +try: + import chainladder as cl + CHAINLADDER_AVAILABLE = True +except ImportError: + CHAINLADDER_AVAILABLE = False + +logger = logging.getLogger(__name__) + +# In-memory storage +triangles_storage: Dict[str, Any] = {} # {triangle_id: {'triangle': cl.Triangle, 'exposure': exposure_data}} + +def dev_select(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Perform development factor selection using chainladder. + + Uses cl.Development for age-to-age factors, LDF, CDF calculation. + """ + try: + if not CHAINLADDER_AVAILABLE: + raise ValueError("chainladder library not available") + + # Extract parameters + triangle_id = input_data['triangle_id'] + averaging = input_data.get('averaging', 'volume') + min_obs = input_data.get('min_obs', 1) + + # Get triangle from storage + if triangle_id not in triangles_storage: + raise ValueError(f"Triangle {triangle_id} not found") + + triangle = triangles_storage[triangle_id]['triangle'] + + # Create and fit Development object + dev = cl.Development(average=averaging, n_periods=min_obs) + dev.fit(triangle) + + # Extract results + age_to_age = dev.ldf_.values.tolist() if hasattr(dev, 'ldf_') else [] + ldf = dev.ldf_.values.tolist() if hasattr(dev, 'ldf_') else [] + cdf = dev.cdf_.values.tolist() if hasattr(dev, 'cdf_') else [] + + return { + "age_to_age": age_to_age, + "LDF": ldf, + "CDF": cdf, + "tail_factor": 1.0, + "diagnostics": { + "method": averaging, + "periods_used": min_obs, + "triangle_shape": triangle.shape + } + } + + except Exception as e: + return { + "age_to_age": None, + "LDF": None, + "CDF": None, + "tail_factor": None, + "diagnostics": {"error": f"Dev select failed: {str(e)}"} + } + +def triangle_build(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Build a chainladder Triangle from input data. + + Required fields: + - rows: List of dictionaries with triangle data + - value_type: 'cumulative' or 'incremental' + - metric: Column name for the metric in the output triangle + + Optional fields: + - exposure: List of exposure values per origin period + - origin_col: Name of origin column in rows (default: 'origin') + - dev_col: Name of development column in rows (default: 'dev') + - value_col: Name of value column in rows (default: 'value') + """ + try: + if not CHAINLADDER_AVAILABLE: + raise ValueError("chainladder library not available") + + # Extract data (no validation - trust input for POC) + rows = input_data['rows'] + value_type = input_data['value_type'] + metric = input_data['metric'] + exposure = input_data.get('exposure', None) # Optional exposure per origin + origin_col = input_data.get('origin_col', 'origin') # TODO: origin and dev need better data type handling + dev_col = input_data.get('dev_col', 'dev') # Default to 'dev' + value_col = input_data.get('value_col', 'value') # Default to 'value' + + df = pd.DataFrame(rows) + df['origin'] = pd.to_datetime(df[origin_col], format='%Y') + df = df.rename(columns={value_col: metric}) + + # Create chainladder Triangle + triangle = cl.Triangle( + df, + origin='origin', + development=dev_col, + columns=[metric], + cumulative=(value_type == 'cumulative') + ) + + if value_type == 'incremental': + triangle = triangle.incr_to_cum() + + # Prepare exposure data if provided + exposure_data = None + if exposure is not None: + if isinstance(exposure, list): + # Create exposure triangle structure + exposure_data = triangle.latest_diagonal * 0 # Zero out existing data + for i, exp_value in enumerate(exposure): + if i < exposure_data.shape[2]: # Don't exceed triangle dimensions + exposure_data.values[0, 0, i] = exp_value + + triangle_id = str(uuid.uuid4()) + triangles_storage[triangle_id] = { + 'triangle': triangle, + 'exposure': exposure_data + } + + return { + "triangle_id": triangle_id, + "profile": { + "n_origin": triangle.shape[0], + "n_dev": triangle.shape[1], + "has_exposure": exposure_data is not None + }, + "warnings": [] + } + + except Exception as e: + return { + "triangle_id": None, + "profile": None, + "warnings": [f"Triangle build failed: {str(e)}"] + } + +def tail_constant(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Apply constant tail factor using cl.TailConstant. + """ + try: + if not CHAINLADDER_AVAILABLE: + raise ValueError("chainladder library not available") + + # Extract parameters + triangle_id = input_data['triangle_id'] + tail_factor = input_data.get('tail_factor', 1.05) + + # Get triangle from storage + if triangle_id not in triangles_storage: + raise ValueError(f"Triangle {triangle_id} not found") + + triangle = triangles_storage[triangle_id]['triangle'] + + # Apply TailConstant + tail = cl.TailConstant(tail_factor) + tail.fit_transform(triangle) + + return { + "tail_factor": float(tail.tail_.values[0, 0]) if hasattr(tail, 'tail_') else tail_factor, + "ldf": tail.ldf_.values.tolist() if hasattr(tail, 'ldf_') else [], + "cdf": tail.cdf_.values.tolist() if hasattr(tail, 'cdf_') else [], + "diagnostics": { + "method": "constant", + "input_factor": tail_factor, + "triangle_shape": triangle.shape + } + } + + except Exception as e: + return { + "tail_factor": None, + "ldf": None, + "cdf": None, + "diagnostics": {"error": f"Tail constant failed: {str(e)}"} + } + +def tail_curve(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Apply curve-fitted tail using cl.TailCurve. + """ + try: + if not CHAINLADDER_AVAILABLE: + raise ValueError("chainladder library not available") + + # Extract parameters + triangle_id = input_data['triangle_id'] + extrap_periods = input_data.get('extrap_periods', 100) + fit_period = input_data.get('fit_period', None) + + # Get triangle from storage + if triangle_id not in triangles_storage: + raise ValueError(f"Triangle {triangle_id} not found") + + triangle = triangles_storage[triangle_id]['triangle'] + + # Apply TailCurve + tail_params = {"extrap_periods": extrap_periods} + if fit_period: + tail_params["fit_period"] = fit_period + + tail = cl.TailCurve(**tail_params) + tail.fit_transform(triangle) + + return { + "tail_factor": float(tail.tail_.values[0, 0]) if hasattr(tail, 'tail_') else None, + "ldf": tail.ldf_.values.tolist() if hasattr(tail, 'ldf_') else [], + "cdf": tail.cdf_.values.tolist() if hasattr(tail, 'cdf_') else [], + "diagnostics": { + "method": "curve", + "extrap_periods": extrap_periods, + "fit_period": fit_period, + "triangle_shape": triangle.shape + } + } + + except Exception as e: + return { + "tail_factor": None, + "ldf": None, + "cdf": None, + "diagnostics": {"error": f"Tail curve failed: {str(e)}"} + } + +def ibnr_estimate(input_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Calculate IBNR and Ultimate using various actuarial methods. + + Available methods: chainladder, bornhuetter_ferguson, benktander, expected_losses + + TODO: case_outstanding, cape_cod, berquist_sherman, frequency_severity + """ + try: + if not CHAINLADDER_AVAILABLE: + raise ValueError("chainladder library not available") + + # Extract parameters + triangle_id = input_data['triangle_id'] + method = input_data.get('method', 'chainladder') + apriori = input_data.get('apriori', None) + n_iters = input_data.get('n_iters', 1) + trend = input_data.get('trend', 0.0) + + # Get triangle from storage + if triangle_id not in triangles_storage: + raise ValueError(f"Triangle {triangle_id} not found") + + triangle_data = triangles_storage[triangle_id] + triangle = triangle_data['triangle'] + stored_exposure = triangle_data.get('exposure') + + # Select and configure method + if method == 'chainladder': + model = cl.Chainladder() + elif method == 'bornhuetter_ferguson': + if apriori is None: + raise ValueError("apriori required for BornhuetterFerguson") + model = cl.BornhuetterFerguson(apriori=apriori) + elif method == 'benktander': + model_params = {'n_iters': n_iters} + if apriori is not None: + model_params['apriori'] = apriori + model = cl.Benktander(**model_params) + elif method == 'expected_losses': + # Expected losses = Benktander with n_iters=0 + if apriori is None: + raise ValueError("apriori required for expected_losses") + model = cl.Benktander(apriori=apriori, n_iters=0) + elif method in ['case_outstanding', 'cape_cod', 'berquist_sherman', 'frequency_severity']: + raise ValueError(f"Method '{method}' not yet implemented - see TODO in MCP tool docstring") + else: + raise ValueError(f"Unknown method: {method}") + + # Fit the model + if method in ['bornhuetter_ferguson', 'benktander', 'expected_losses']: + if stored_exposure is not None: + model.fit(triangle, sample_weight=stored_exposure) + else: + raise ValueError(f"{method} method requires exposure data in triangle_build") + else: + model.fit(triangle) + + # Extract results + ultimate = model.ultimate_.values.tolist() if hasattr(model, 'ultimate_') else [] + ibnr = model.ibnr_.values.tolist() if hasattr(model, 'ibnr_') else [] + + # Additional method-specific outputs + diagnostics = { + "method": method, + "triangle_shape": triangle.shape + } + + if hasattr(model, 'apriori_'): + diagnostics["apriori"] = float(model.apriori_.values[0, 0]) + if method == 'benktander' or method == 'expected_losses': + diagnostics["n_iters"] = n_iters + if method == 'cape_cod' and trend != 0.0: + diagnostics["trend"] = trend + + return { + "ultimate": ultimate, + "ibnr": ibnr, + "latest_diagonal": triangle.latest_diagonal.values.tolist() if hasattr(triangle, 'latest_diagonal') else [], + "diagnostics": diagnostics + } + + except Exception as e: + return { + "ultimate": None, + "ibnr": None, + "latest_diagonal": None, + "diagnostics": {"error": f"IBNR estimation failed: {str(e)}"} + } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c5072d5d9..35ed0e640 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,8 @@ scipy>=1.13.0 # RAG dependencies sqlite-vec>=0.1.6 -pypdf>=6.0.0 \ No newline at end of file +pypdf>=6.0.0 + +# Actuarial dependencies +chainladder>=0.8.21 +pydantic>=2.0.0 diff --git a/src/core/prompts/system-prompt/variants/generic/template.ts b/src/core/prompts/system-prompt/variants/generic/template.ts index a2907aab7..679bfab45 100644 --- a/src/core/prompts/system-prompt/variants/generic/template.ts +++ b/src/core/prompts/system-prompt/variants/generic/template.ts @@ -79,4 +79,36 @@ When a server is connected, you can use the server's tools via the \`use_mcp_too 3. Incorporate findings into your response 4. Provide both theoretical context from papers and practical application +## Actuarial Computation Protocol + +**CRITICAL**: When users need actuarial calculations, triangle analysis, or IBNR estimation, you MUST use the actuarial computation tools from the "Actuarial-RAG" MCP server: + +**Available Computation Tools**: +- **triangle_build_tool**: Convert loss data to actuarial triangles (with optional exposure data) +- **dev_select_tool**: Development factor selection (volume/simple/median averaging) +- **tail_constant_tool**: Apply constant tail factors +- **tail_curve_tool**: Apply curve-fitted tail factors +- **ibnr_estimate_tool**: Calculate IBNR & Ultimate (chainladder, BornhuetterFerguson, Benktander, expected_losses) + +**Trigger Conditions**: Use computation tools when users need: +- Triangle creation from loss/claims data +- Development pattern analysis +- Tail factor estimation +- IBNR/Ultimate loss projections +- Actuarial method comparisons +- Loss reserving calculations +- Triangle manipulation or analysis + +**Computation Workflow**: +1. **Build Triangle**: Use triangle_build_tool to convert raw data into chainladder format +2. **Analyze Development**: Use dev_select_tool for development factor patterns +3. **Apply Tail**: Use tail_constant_tool or tail_curve_tool for tail estimation +4. **Calculate IBNR**: Use ibnr_estimate_tool with appropriate method (chainladder, BF, etc.) +5. **Validate with RAG**: Cross-reference results with academic papers using RAG tools + +**Integration Strategy**: +- Use RAG tools for theoretical background and validation +- Use computation tools for actual numerical analysis +- Combine both for comprehensive actuarial solutions + {{MCP_SERVERS_LIST}}` diff --git a/src/core/prompts/system-prompt/variants/next-gen/template.ts b/src/core/prompts/system-prompt/variants/next-gen/template.ts index fc7626987..0fe5e8534 100644 --- a/src/core/prompts/system-prompt/variants/next-gen/template.ts +++ b/src/core/prompts/system-prompt/variants/next-gen/template.ts @@ -50,7 +50,10 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}} export const rules_template = `RULES -- **ACTUARIAL ANALYSIS REQUIREMENT**: When ANY user request contains actuarial terminology, insurance concepts, loss reserving, risk assessment, or mentions actuarial methods/standards, you MUST IMMEDIATELY use the RAG MCP tools before responding. This is MANDATORY - no exceptions. Use the "Actuarial-RAG" server with tools: search_friedland_paper, search_werner_modlin_paper, or search_both_papers to cross-reference the academic literature first. +- **ACTUARIAL ANALYSIS REQUIREMENT**: When ANY user request contains actuarial terminology, insurance concepts, loss reserving, risk assessment, or mentions actuarial methods/standards, you MUST use the appropriate MCP tools from the "Actuarial-RAG" server. This is MANDATORY - no exceptions. + - For theoretical context: Use RAG tools (search_friedland_paper, search_werner_modlin_paper, search_both_papers) + - For calculations: Use computation tools (triangle_build_tool, dev_select_tool, tail_constant_tool, tail_curve_tool, ibnr_estimate_tool) + - When users provide loss data or request actuarial calculations, prioritize computation tools followed by RAG validation - Your current working directory is: {{CWD}} - You cannot \`cd\` into a different directory to complete a task. You are stuck operating from '{{CWD}}', so be sure to pass in the correct 'path' parameter when using tools that require a path. @@ -110,4 +113,36 @@ When a server is connected, you can use the server's tools via the \`use_mcp_too 3. Incorporate findings into your response 4. Provide both theoretical context from papers and practical application +## Actuarial Computation Protocol + +**CRITICAL**: When users need actuarial calculations, triangle analysis, or IBNR estimation, you MUST use the actuarial computation tools from the "Actuarial-RAG" MCP server: + +**Available Computation Tools**: +- **triangle_build_tool**: Convert loss data to actuarial triangles (with optional exposure data) +- **dev_select_tool**: Development factor selection (volume/simple/median averaging) +- **tail_constant_tool**: Apply constant tail factors +- **tail_curve_tool**: Apply curve-fitted tail factors +- **ibnr_estimate_tool**: Calculate IBNR & Ultimate (chainladder, BornhuetterFerguson, Benktander, expected_losses) + +**Trigger Conditions**: Use computation tools when users need: +- Triangle creation from loss/claims data +- Development pattern analysis +- Tail factor estimation +- IBNR/Ultimate loss projections +- Actuarial method comparisons +- Loss reserving calculations +- Triangle manipulation or analysis + +**Computation Workflow**: +1. **Build Triangle**: Use triangle_build_tool to convert raw data into chainladder format +2. **Analyze Development**: Use dev_select_tool for development factor patterns +3. **Apply Tail**: Use tail_constant_tool or tail_curve_tool for tail estimation +4. **Calculate IBNR**: Use ibnr_estimate_tool with appropriate method (chainladder, BF, etc.) +5. **Validate with RAG**: Cross-reference results with academic papers using RAG tools + +**Integration Strategy**: +- Use RAG tools for theoretical background and validation +- Use computation tools for actual numerical analysis +- Combine both for comprehensive actuarial solutions + {{MCP_SERVERS_LIST}}`