diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..2087cec138
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,464 @@
+# EditorConfig to support per-solution formatting.
+# Use the EditorConfig VS add-in to make this work.
+# http://editorconfig.org/
+#
+# Resources for what's supported for .NET/C#
+# https://kent-boogaart.com/blog/editorconfig-reference-for-c-developers
+# https://learn.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference
+
+root = true
+
+[*]
+indent_style = space
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.cs]
+indent_size = 4
+dotnet_sort_system_directives_first = true
+
+# Don't use this. qualifier
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+
+# use int x = .. over Int32
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+
+# use int.MaxValue over Int32.MaxValue
+dotnet_style_predefined_type_for_member_access = true:warning
+
+# Require var all the time (modern C#)
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = true:warning
+
+# Disallow throw expressions in most cases
+csharp_style_throw_expression = false:suggestion
+
+# Newline settings - Allman style (braces on new line)
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+
+# Indentation settings
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = no_change
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents_when_block = false
+
+# Spacing settings
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_around_declaration_statements = false
+csharp_space_before_open_square_brackets = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_square_brackets = false
+
+# Wrapping settings
+csharp_preserve_single_line_statements = false
+csharp_preserve_single_line_blocks = true
+
+# Namespace settings - C# 10+ file-scoped namespaces
+csharp_style_namespace_declarations = file_scoped:warning
+
+# Brace settings - ALWAYS use braces, even for single-line blocks
+csharp_prefer_braces = true:warning
+
+# Expression-bodied members (modern C#)
+csharp_style_expression_bodied_methods = when_on_single_line:suggestion
+csharp_style_expression_bodied_constructors = false:suggestion
+csharp_style_expression_bodied_operators = when_on_single_line:suggestion
+csharp_style_expression_bodied_properties = true:suggestion
+csharp_style_expression_bodied_indexers = true:suggestion
+csharp_style_expression_bodied_accessors = true:suggestion
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion
+
+# Pattern matching (C# 7+)
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_prefer_switch_expression = true:suggestion
+csharp_style_prefer_pattern_matching = true:suggestion
+csharp_style_prefer_not_pattern = true:warning
+csharp_style_prefer_extended_property_pattern = true:suggestion
+
+# Null checking (C# 6+)
+csharp_style_conditional_delegate_call = true:warning
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+
+# Modern C# features
+csharp_prefer_simple_using_statement = true:warning
+csharp_style_prefer_method_group_conversion = true:suggestion
+csharp_style_prefer_top_level_statements = false:suggestion
+csharp_style_prefer_primary_constructors = true:warning
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_tuple_swap = true:warning
+csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_prefer_readonly_struct = true:warning
+csharp_style_prefer_readonly_struct_member = true:warning
+
+# Use collection expressions (C# 12+)
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+
+# Code block preferences
+csharp_prefer_simple_default_expression = true:suggestion
+dotnet_style_prefer_compound_assignment = true:warning
+dotnet_style_prefer_simplified_boolean_expressions = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
+dotnet_style_prefer_conditional_expression_over_return = false:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# Object/collection initializers
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_explicit_tuple_names = true:warning
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:warning
+csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+
+# this. and Me. preferences - don't use this. unless required
+dotnet_style_qualification_for_event = false:warning
+dotnet_style_qualification_for_method = false:warning
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+dotnet_style_readonly_field = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
+
+# Parentheses preferences - be explicit for clarity
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
+
+# Naming conventions - name all constant fields using PascalCase
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = warning
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# internal and private fields should be _camelCase
+dotnet_naming_rule.camel_case_for_private_internal_fields.severity = warning
+dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
+dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
+dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
+dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
+dotnet_naming_style.camel_case_underscore_style.required_prefix = _
+dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
+
+# Async methods should end with Async
+dotnet_naming_rule.async_methods_should_end_with_async.severity = warning
+dotnet_naming_rule.async_methods_should_end_with_async.symbols = async_methods
+dotnet_naming_rule.async_methods_should_end_with_async.style = end_in_async_style
+dotnet_naming_symbols.async_methods.applicable_kinds = method
+dotnet_naming_symbols.async_methods.required_modifiers = async
+dotnet_naming_style.end_in_async_style.required_suffix = Async
+dotnet_naming_style.end_in_async_style.capitalization = pascal_case
+
+# Interfaces should start with I
+dotnet_naming_rule.interfaces_should_start_with_i.severity = warning
+dotnet_naming_rule.interfaces_should_start_with_i.symbols = interfaces
+dotnet_naming_rule.interfaces_should_start_with_i.style = i_prefix_style
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_style.i_prefix_style.required_prefix = I
+dotnet_naming_style.i_prefix_style.capitalization = pascal_case
+
+[*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}]
+indent_size = 2
+
+[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+[*.json]
+indent_size = 2
+
+[*.{ps1,psm1}]
+indent_size = 4
+
+[*.sh]
+indent_size = 4
+end_of_line = lf
+
+[*.{razor,cshtml}]
+charset = utf-8-bom
+
+[*.{cs,vb}]
+
+# SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
+dotnet_diagnostic.SYSLIB1054.severity = warning
+
+# CA1018: Mark attributes with AttributeUsageAttribute
+dotnet_diagnostic.CA1018.severity = warning
+
+# CA1047: Do not declare protected member in sealed type
+dotnet_diagnostic.CA1047.severity = warning
+
+# CA1305: Specify IFormatProvider
+dotnet_diagnostic.CA1305.severity = warning
+
+# CA1507: Use nameof to express symbol names (critical for AGENTS.md)
+dotnet_diagnostic.CA1507.severity = warning
+
+# CA1510: Use ArgumentNullException throw helper
+dotnet_diagnostic.CA1510.severity = warning
+
+# CA1511: Use ArgumentException throw helper
+dotnet_diagnostic.CA1511.severity = warning
+
+# CA1512: Use ArgumentOutOfRangeException throw helper
+dotnet_diagnostic.CA1512.severity = warning
+
+# CA1513: Use ObjectDisposedException throw helper
+dotnet_diagnostic.CA1513.severity = warning
+
+# CA1725: Parameter names should match base declaration
+dotnet_diagnostic.CA1725.severity = suggestion
+
+# CA1802: Use literals where appropriate
+dotnet_diagnostic.CA1802.severity = warning
+
+# CA1805: Do not initialize unnecessarily
+dotnet_diagnostic.CA1805.severity = warning
+
+# CA1810: Do not initialize static fields unnecessarily
+dotnet_diagnostic.CA1810.severity = warning
+
+# CA1821: Remove empty Finalizers
+dotnet_diagnostic.CA1821.severity = warning
+
+# CA1822: Make member static
+dotnet_diagnostic.CA1822.severity = warning
+dotnet_code_quality.CA1822.api_surface = private, internal
+
+# CA1823: Avoid unused private fields
+dotnet_diagnostic.CA1823.severity = warning
+
+# CA1825: Avoid zero-length array allocations
+dotnet_diagnostic.CA1825.severity = warning
+
+# CA1826: Do not use Enumerable methods on indexable collections
+dotnet_diagnostic.CA1826.severity = warning
+
+# CA1827: Do not use Count() or LongCount() when Any() can be used
+dotnet_diagnostic.CA1827.severity = warning
+
+# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used
+dotnet_diagnostic.CA1828.severity = warning
+
+# CA1829: Use Length/Count property instead of Count() when available
+dotnet_diagnostic.CA1829.severity = warning
+
+# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder
+dotnet_diagnostic.CA1830.severity = warning
+
+# CA1831-CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate
+dotnet_diagnostic.CA1831.severity = warning
+dotnet_diagnostic.CA1832.severity = warning
+dotnet_diagnostic.CA1833.severity = warning
+
+# CA1834: Consider using 'StringBuilder.Append(char)' when applicable
+dotnet_diagnostic.CA1834.severity = warning
+
+# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
+dotnet_diagnostic.CA1835.severity = warning
+
+# CA1836: Prefer IsEmpty over Count
+dotnet_diagnostic.CA1836.severity = warning
+
+# CA1837: Use 'Environment.ProcessId'
+dotnet_diagnostic.CA1837.severity = warning
+
+# CA1838: Avoid 'StringBuilder' parameters for P/Invokes
+dotnet_diagnostic.CA1838.severity = warning
+
+# CA1839: Use 'Environment.ProcessPath'
+dotnet_diagnostic.CA1839.severity = warning
+
+# CA1840: Use 'Environment.CurrentManagedThreadId'
+dotnet_diagnostic.CA1840.severity = warning
+
+# CA1841: Prefer Dictionary.Contains methods
+dotnet_diagnostic.CA1841.severity = warning
+
+# CA1842: Do not use 'WhenAll' with a single task
+dotnet_diagnostic.CA1842.severity = warning
+
+# CA1843: Do not use 'WaitAll' with a single task
+dotnet_diagnostic.CA1843.severity = warning
+
+# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream'
+dotnet_diagnostic.CA1844.severity = warning
+
+# CA1845: Use span-based 'string.Concat'
+dotnet_diagnostic.CA1845.severity = warning
+
+# CA1846: Prefer AsSpan over Substring
+dotnet_diagnostic.CA1846.severity = warning
+
+# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
+dotnet_diagnostic.CA1847.severity = warning
+
+# CA1852: Seal internal types
+dotnet_diagnostic.CA1852.severity = warning
+
+# CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
+dotnet_diagnostic.CA1854.severity = warning
+
+# CA1855: Prefer 'Clear' over 'Fill'
+dotnet_diagnostic.CA1855.severity = warning
+
+# CA1856: Incorrect usage of ConstantExpected attribute
+dotnet_diagnostic.CA1856.severity = error
+
+# CA1857: A constant is expected for the parameter
+dotnet_diagnostic.CA1857.severity = warning
+
+# CA1858: Use 'StartsWith' instead of 'IndexOf'
+dotnet_diagnostic.CA1858.severity = warning
+
+# CA2007: DISABLED - Never use ConfigureAwait(false) per AGENTS.md
+dotnet_diagnostic.CA2007.severity = none
+
+# CA2008: Do not create tasks without passing a TaskScheduler
+dotnet_diagnostic.CA2008.severity = warning
+
+# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value
+dotnet_diagnostic.CA2009.severity = warning
+
+# CA2011: Avoid infinite recursion
+dotnet_diagnostic.CA2011.severity = warning
+
+# CA2012: Use ValueTask correctly
+dotnet_diagnostic.CA2012.severity = warning
+
+# CA2013: Do not use ReferenceEquals with value types
+dotnet_diagnostic.CA2013.severity = warning
+
+# CA2014: Do not use stackalloc in loops
+dotnet_diagnostic.CA2014.severity = warning
+
+# CA2016: Forward the 'CancellationToken' parameter to methods that take one
+dotnet_diagnostic.CA2016.severity = warning
+
+# CA2022: Avoid inexact read with `Stream.Read`
+dotnet_diagnostic.CA2022.severity = warning
+
+# CA2200: Rethrow to preserve stack details
+dotnet_diagnostic.CA2200.severity = warning
+
+# CA2201: Do not raise reserved exception types
+dotnet_diagnostic.CA2201.severity = warning
+
+# CA2208: Instantiate argument exceptions correctly
+dotnet_diagnostic.CA2208.severity = warning
+
+# CA2245: Do not assign a property to itself
+dotnet_diagnostic.CA2245.severity = warning
+
+# CA2246: Assigning symbol and its member in the same statement
+dotnet_diagnostic.CA2246.severity = warning
+
+# CA2249: Use string.Contains instead of string.IndexOf to improve readability
+dotnet_diagnostic.CA2249.severity = warning
+
+# IDE0005: Remove unnecessary usings
+dotnet_diagnostic.IDE0005.severity = warning
+
+# IDE0011: Curly braces to surround blocks of code
+dotnet_diagnostic.IDE0011.severity = warning
+
+# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable)
+dotnet_diagnostic.IDE0020.severity = warning
+
+# IDE0029: Use coalesce expression (non-nullable types)
+dotnet_diagnostic.IDE0029.severity = warning
+
+# IDE0030: Use coalesce expression (nullable types)
+dotnet_diagnostic.IDE0030.severity = warning
+
+# IDE0031: Use null propagation
+dotnet_diagnostic.IDE0031.severity = warning
+
+# IDE0035: Remove unreachable code
+dotnet_diagnostic.IDE0035.severity = warning
+
+# IDE0036: Order modifiers
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
+dotnet_diagnostic.IDE0036.severity = warning
+
+# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable)
+dotnet_diagnostic.IDE0038.severity = warning
+
+# IDE0043: Format string contains invalid placeholder
+dotnet_diagnostic.IDE0043.severity = warning
+
+# IDE0044: Make field readonly
+dotnet_diagnostic.IDE0044.severity = warning
+
+# IDE0051: Remove unused private members
+dotnet_diagnostic.IDE0051.severity = warning
+
+# IDE0055: All formatting rules
+dotnet_diagnostic.IDE0055.severity = suggestion
+
+# IDE0059: Unnecessary assignment to a value
+dotnet_diagnostic.IDE0059.severity = warning
+
+# IDE0060: Remove unused parameter
+dotnet_code_quality_unused_parameters = non_public
+dotnet_diagnostic.IDE0060.severity = warning
+
+# IDE0062: Make local function static
+dotnet_diagnostic.IDE0062.severity = warning
+
+# IDE0073: File header - disabled
+dotnet_diagnostic.IDE0073.severity = none
+
+# IDE0161: Convert to file-scoped namespace
+dotnet_diagnostic.IDE0161.severity = warning
+
+# IDE0200: Lambda expression can be removed
+dotnet_diagnostic.IDE0200.severity = warning
+
+# IDE2000: Disallow multiple blank lines
+dotnet_style_allow_multiple_blank_lines_experimental = false
+dotnet_diagnostic.IDE2000.severity = warning
+
+# Verify settings for test files
+[*.{received,verified}.{txt,xml,json}]
+charset = utf-8-bom
+end_of_line = lf
+indent_size = unset
+indent_style = unset
+insert_final_newline = false
+tab_width = unset
+trim_trailing_whitespace = false
diff --git a/AGENTS.md b/AGENTS.md
index 3c49ac5052..3ed5071350 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,9 +1,12 @@
# Repository Guidelines
-# Rules to follow
+## Rules to follow
- Always run `dotnet build GraphRag.slnx` (or the relevant project) before executing any `dotnet test` command.
- Default to the latest available versions (e.g., Apache AGE `latest`) when selecting dependencies, per user request ("тобі треба latest").
- Do not create or rely on fake database stores (e.g., `FakePostgresGraphStore`); all tests must use real connectors/backing services.
+- Keep default prompts in static C# classes; do not rely on prompt files under `prompts/` for built-in templates.
+- Register language models through Microsoft.Extensions.AI keyed services; avoid bespoke `LanguageModelConfig` providers.
+- Always run `dotnet format GraphRag.slnx` before finishing work.
# Conversations
any resulting updates to agents.md should go under the section "## Rules to follow"
diff --git a/Directory.Build.props b/Directory.Build.props
index ab2b8f05fe..58057d740e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -25,8 +25,8 @@
https://github.com/managedcode/graphrag
https://github.com/managedcode/graphrag
Managed Code GraphRag
- 0.0.2
- 0.0.2
+ 0.0.3
+ 0.0.3
@@ -42,7 +42,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index dcd78bb678..b214525f9f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,6 +3,7 @@
+
@@ -20,4 +21,4 @@
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index 721cd314ac..e0afbcbeab 100644
--- a/README.md
+++ b/README.md
@@ -86,10 +86,72 @@ graphrag/
## Integration Testing Strategy
-- **No fakes.** We removed the legacy fake Postgres store. Every graph operation in tests uses real services orchestrated by Testcontainers.
-- **Security coverage.** `Integration/PostgresGraphStoreIntegrationTests.cs` includes payloads that mimic SQL/Cypher injection attempts to ensure values remain literals and labels/types are strictly validated.
-- **Cross-backend validation.** `Integration/GraphStoreIntegrationTests.cs` exercises Postgres, Neo4j, and Cosmos (when available) through the shared `IGraphStore` abstraction.
+- **No fakes.** We removed the legacy fake Postgres store. Every graph operation in tests uses real services orchestrated by Testcontainers.
+- **Security coverage.** `Integration/PostgresGraphStoreIntegrationTests.cs` includes payloads that mimic SQL/Cypher injection attempts to ensure values remain literals and labels/types are strictly validated.
+- **Cross-backend validation.** `Integration/GraphStoreIntegrationTests.cs` exercises Postgres, Neo4j, and Cosmos (when available) through the shared `IGraphStore` abstraction.
- **Workflow smoke tests.** Pipelines (e.g., `IndexingPipelineRunnerTests`) and finalization steps run end-to-end with the fixture-provisioned infrastructure.
+- **Prompt precedence.** `Integration/CommunitySummariesIntegrationTests.cs` proves manual prompt overrides win over auto-tuned assets while still falling back to auto templates when manual text is absent.
+- **Callback and stats instrumentation.** `Runtime/PipelineExecutorTests.cs` now asserts that pipeline callbacks fire and runtime statistics are captured even when workflows fail early, so custom telemetry remains reliable.
+
+---
+
+## Pipeline Cache
+
+Pipelines exchange state through the `IPipelineCache` abstraction. Every workflow step receives the same cache instance via `PipelineRunContext`, so it can reuse expensive results (LLM calls, chunk expansions, graph lookups) that were produced earlier in the run instead of recomputing them. The cache also keeps optional debug payloads per entry so you can persist trace metadata alongside the main value.
+
+To use the built-in in-memory cache, register it alongside the standard ASP.NET Core services:
+
+```csharp
+using GraphRag.Cache;
+
+builder.Services.AddMemoryCache();
+builder.Services.AddSingleton();
+```
+
+Prefer a different backend? Implement `IPipelineCache` yourself and register it through DI—the pipeline will pick up your custom cache automatically.
+
+- **Per-scope isolation.** `MemoryPipelineCache.CreateChild("stage")` scopes keys by prefix (`parent:stage:key`). Calling `ClearAsync` on the parent removes every nested key, so multi-step workflows do not leak data between stages.
+- **Debug traces.** The cache stores optional debug payloads per entry; `DeleteAsync` and `ClearAsync` always clear these traces, preventing the diagnostic dictionary from growing unbounded.
+- **Lifecycle guidance.** Create the root cache once per pipeline run (the default context factory does this for you) and spawn children inside individual workflows when you need an isolated namespace.
+
+---
+
+## Language Model Registration
+
+GraphRAG delegates language-model configuration to [Microsoft.Extensions.AI](https://learn.microsoft.com/dotnet/ai/overview). Register keyed clients for every `ModelId` you reference in configuration—pick any string key that matches your config:
+
+```csharp
+using Azure;
+using Azure.AI.OpenAI;
+using GraphRag.Config;
+using Microsoft.Extensions.AI;
+
+var openAi = new OpenAIClient(new Uri(endpoint), new AzureKeyCredential(key));
+const string chatModelId = "chat_model";
+const string embeddingModelId = "embedding_model";
+
+builder.Services.AddKeyedSingleton(
+ chatModelId,
+ _ => openAi.GetChatClient(chatDeployment));
+
+builder.Services.AddKeyedSingleton>(
+ embeddingModelId,
+ _ => openAi.GetEmbeddingClient(embeddingDeployment));
+```
+
+Rate limits, retries, and other policies should be configured when you create these clients (for example by wrapping them with `Polly` handlers). `GraphRagConfig.Models` simply tracks the set of model keys that have been registered so overrides can validate references.
+
+---
+
+## Indexing, Querying, and Prompt Tuning Alignment
+
+The .NET port mirrors the [GraphRAG indexing architecture](https://microsoft.github.io/graphrag/index/overview/) and its query workflows so downstream applications retain parity with the Python reference implementation.
+
+- **Indexing overview.** Workflows such as `extract_graph`, `create_communities`, and `community_summaries` map 1:1 to the [default data flow](https://microsoft.github.io/graphrag/index/default_dataflow/) and persist the same tables (`text_units`, `entities`, `relationships`, `communities`, `community_reports`, `covariates`). The new prompt template loader honours manual or auto-tuned prompts before falling back to the stock templates in `prompts/`.
+- **Query capabilities.** The query pipeline retains global search, local search, drift search, and question generation semantics described in the [GraphRAG query overview](https://microsoft.github.io/graphrag/query/overview/). Each orchestrator continues to assemble context from the indexed tables so you can reference [global](https://microsoft.github.io/graphrag/query/global_search/) or [local](https://microsoft.github.io/graphrag/query/local_search/) narratives interchangeably.
+- **Prompt tuning.** GraphRAG’s [manual](https://microsoft.github.io/graphrag/prompt_tuning/manual_prompt_tuning/) and [auto](https://microsoft.github.io/graphrag/prompt_tuning/auto_prompt_tuning/) strategies are surfaced through `GraphRagConfig.PromptTuning`. Store custom templates under `prompts/` or point `PromptTuning.Manual.Directory`/`PromptTuning.Auto.Directory` at your tuning outputs. You can also skip files entirely by assigning inline text (multi-line or prefixed with `inline:`) to workflow prompt properties. Stage keys and placeholders are documented in `docs/indexing-and-query.md`.
+
+See [`docs/indexing-and-query.md`](docs/indexing-and-query.md) for a deeper mapping between the .NET workflows and the research publications underpinning GraphRAG.
---
diff --git a/docs/indexing-and-query.md b/docs/indexing-and-query.md
new file mode 100644
index 0000000000..6e7df9e8d5
--- /dev/null
+++ b/docs/indexing-and-query.md
@@ -0,0 +1,99 @@
+# Indexing, Querying, and Prompt Tuning in GraphRAG for .NET
+
+GraphRAG for .NET keeps feature parity with the Python reference project described in the [Microsoft Research blog](https://www.microsoft.com/en-us/research/blog/graphrag-unlocking-llm-discovery-on-narrative-private-data/) and the [GraphRAG paper](https://arxiv.org/pdf/2404.16130). This document explains how the .NET workflows map to the concepts documented on [microsoft.github.io/graphrag](https://microsoft.github.io/graphrag/), highlights the supported query modes, and shows how to customise prompts via manual or auto tuning outputs.
+
+## Indexing Architecture
+
+- **Workflow parity.** Each indexing stage matches the Python pipeline and the [default data flow](https://microsoft.github.io/graphrag/index/default_dataflow/):
+ - `load_input_documents` → `create_base_text_units` → `summarize_descriptions`
+ - `extract_graph` persists `entities` and `relationships`
+ - `create_communities` produces `communities`
+ - `community_summaries` writes `community_reports`
+ - `extract_covariates` stores `covariates`
+- **Storage schema.** Tables share the column layout described under [index outputs](https://microsoft.github.io/graphrag/index/outputs/). The new strongly-typed records (`CommunityRecord`, `CovariateRecord`, etc.) mirror the JSON representation used by the Python implementation.
+- **Cluster configuration.** `GraphRagConfig.ClusterGraph` exposes the same knobs as the Python `cluster_graph` settings, enabling largest-component filtering and deterministic seeding.
+
+## Language Model Registration
+
+Workflows resolve language models from the DI container via [Microsoft.Extensions.AI](https://learn.microsoft.com/dotnet/ai/overview). Register keyed services for every `ModelId` you plan to reference:
+
+```csharp
+using Azure;
+using Azure.AI.OpenAI;
+using GraphRag.Config;
+using Microsoft.Extensions.AI;
+
+var openAi = new OpenAIClient(new Uri(endpoint), new AzureKeyCredential(key));
+const string chatModelId = "chat_model";
+const string embeddingModelId = "embedding_model";
+
+services.AddKeyedSingleton(chatModelId, _ => openAi.GetChatClient(chatDeployment));
+services.AddKeyedSingleton>(embeddingModelId, _ => openAi.GetEmbeddingClient(embeddingDeployment));
+```
+
+Configure retries, rate limits, and logging when you construct the concrete clients. `GraphRagConfig.Models` simply records the set of registered keys so configuration overrides can validate references.
+
+## Pipeline Cache
+
+`IPipelineCache` is intentionally infrastructure-neutral. To mirror ASP.NET Core's in-memory behaviour, register the built-in cache services alongside the provided adapter:
+
+```csharp
+services.AddMemoryCache();
+services.AddSingleton();
+```
+
+Need Redis or something else? Implement `IPipelineCache` yourself and register it through DI; the pipeline will automatically consume your custom cache.
+
+## Query Capabilities
+
+The query layer ports the orchestrators documented in the [GraphRAG query overview](https://microsoft.github.io/graphrag/query/overview/):
+
+- **Global search** ([docs](https://microsoft.github.io/graphrag/query/global_search/)) traverses community summaries and graph context to craft answers spanning the corpus.
+- **Local search** ([docs](https://microsoft.github.io/graphrag/query/local_search/)) anchors on a document neighbourhood when you need focused context.
+- **Drift search** ([docs](https://microsoft.github.io/graphrag/query/drift_search/)) monitors narrative changes across time slices.
+- **Question generation** ([docs](https://microsoft.github.io/graphrag/query/question_generation/)) produces follow-up questions to extend an investigation.
+
+Every orchestrator consumes the same indexed tables as the Python project, so the .NET stack interoperates with BYOG scenarios described in the [index architecture guide](https://microsoft.github.io/graphrag/index/architecture/).
+
+## Prompt Tuning
+
+Manual and auto prompt tuning are both available without code changes:
+
+1. **Manual overrides** follow the rules from [manual prompt tuning](https://microsoft.github.io/graphrag/prompt_tuning/manual_prompt_tuning/).
+ - Place custom templates under a directory referenced by `GraphRagConfig.PromptTuning.Manual.Directory` and set `Enabled = true`.
+ - Filenames follow the stage key pattern `section/workflow/kind.txt` (see table below).
+2. **Auto tuning** integrates the outputs documented in [auto prompt tuning](https://microsoft.github.io/graphrag/prompt_tuning/auto_prompt_tuning/).
+ - Point `GraphRagConfig.PromptTuning.Auto.Directory` at the folder containing the generated prompts and set `Enabled = true`.
+ - The runtime prefers explicit paths from workflow configs, then manual overrides, then auto-tuned files, and finally the built-in defaults in `prompts/`.
+3. **Inline overrides** can be injected directly from code: set `ExtractGraphConfig.SystemPrompt`, `ExtractGraphConfig.Prompt`, or the equivalent properties to either a multi-line string or a value prefixed with `inline:`. Inline values bypass template file lookups and are used as-is.
+
+### Stage Keys and Placeholders
+
+| Workflow | Stage key | Purpose | Supported placeholders |
+|----------|-----------|---------|------------------------|
+| `extract_graph` (system) | `index/extract_graph/system.txt` | System prompt that instructs the extractor. | _N/A_ |
+| `extract_graph` (user) | `index/extract_graph/user.txt` | User prompt template for individual text units. | `{{max_entities}}`, `{{text}}` |
+| `community_summaries` (system) | `index/community_reports/system.txt` | System guidance for cluster summarisation. | _N/A_ |
+| `community_summaries` (user) | `index/community_reports/user.txt` | User prompt template for entity lists. | `{{max_length}}`, `{{entities}}` |
+
+Placeholders are replaced at runtime with values drawn from workflow configuration:
+
+- `{{max_entities}}` → `ExtractGraphConfig.EntityTypes.Count + 5` (minimum 1)
+- `{{text}}` → the original text unit content
+- `{{max_length}}` → `CommunityReportsConfig.MaxLength`
+- `{{entities}}` → bullet list of entity titles and descriptions
+
+If a template is omitted, the runtime falls back to the built-in prompts defined in `GraphRagPromptLibrary`.
+
+## Integration Tests
+
+`tests/ManagedCode.GraphRag.Tests/Integration/CommunitySummariesIntegrationTests.cs` exercises the new prompt loader end-to-end using the file-backed pipeline storage. Combined with the existing Aspire-powered suites, the tests demonstrate how indexing, community detection, and summarisation behave with tuned prompts while remaining faithful to the [GraphRAG BYOG guidance](https://microsoft.github.io/graphrag/index/byog/).
+
+## Further Reading
+
+- [GraphRAG prompt tuning overview](https://microsoft.github.io/graphrag/prompt_tuning/overview/)
+- [GraphRAG index methods](https://microsoft.github.io/graphrag/index/methods/)
+- [GraphRAG query overview](https://microsoft.github.io/graphrag/query/overview/)
+- [GraphRAG default dataflow](https://microsoft.github.io/graphrag/index/default_dataflow/)
+
+These resources underpin the .NET implementation and provide broader context for customising or extending the library.
diff --git a/prompts/community_graph.txt b/prompts/community_graph.txt
new file mode 100644
index 0000000000..db1370edae
--- /dev/null
+++ b/prompts/community_graph.txt
@@ -0,0 +1,2 @@
+You are an investigative analyst. Produce concise, neutral summaries that describe the shared theme binding the supplied entities.
+Highlight how they relate, why the cluster matters, and any notable signals the reader should know. Do not invent facts.
diff --git a/prompts/community_text.txt b/prompts/community_text.txt
new file mode 100644
index 0000000000..4080cdac7b
--- /dev/null
+++ b/prompts/community_text.txt
@@ -0,0 +1,6 @@
+Summarise the key theme that connects the following entities in no more than {{max_length}} characters. Focus on what unites them and why the group matters. Avoid bullet lists.
+
+Entities:
+{{entities}}
+
+Provide a single paragraph answer.
diff --git a/prompts/index/extract_graph.system.txt b/prompts/index/extract_graph.system.txt
new file mode 100644
index 0000000000..bdc048e535
--- /dev/null
+++ b/prompts/index/extract_graph.system.txt
@@ -0,0 +1,9 @@
+You are a precise information extraction engine. Analyse the supplied text and return structured JSON describing:
+- distinct entities (people, organisations, locations, products, events, concepts, technologies, dates, other)
+- relationships between those entities
+
+Rules:
+- Only use information explicitly stated or implied in the text.
+- Prefer short, human-readable titles.
+- Use snake_case relationship types (e.g., "works_with", "located_in").
+- Always return valid JSON adhering to the response schema.
diff --git a/prompts/index/extract_graph.user.txt b/prompts/index/extract_graph.user.txt
new file mode 100644
index 0000000000..506c606aac
--- /dev/null
+++ b/prompts/index/extract_graph.user.txt
@@ -0,0 +1,28 @@
+Extract up to {{max_entities}} of the most important entities and their relationships from the following text.
+
+Text (between and markers):
+
+{{text}}
+
+
+Respond with JSON matching this schema:
+{
+ "entities": [
+ {
+ "title": "string",
+ "type": "person | organization | location | product | event | concept | technology | date | other",
+ "description": "short description",
+ "confidence": 0.0 - 1.0
+ }
+ ],
+ "relationships": [
+ {
+ "source": "entity title",
+ "target": "entity title",
+ "type": "relationship_type",
+ "description": "short description",
+ "weight": 0.0 - 1.0,
+ "bidirectional": true | false
+ }
+ ]
+}
diff --git a/src/ManagedCode.GraphRag.CosmosDb/CosmosGraphStore.cs b/src/ManagedCode.GraphRag.CosmosDb/CosmosGraphStore.cs
index 0914edea61..95955af584 100644
--- a/src/ManagedCode.GraphRag.CosmosDb/CosmosGraphStore.cs
+++ b/src/ManagedCode.GraphRag.CosmosDb/CosmosGraphStore.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Runtime.CompilerServices;
-using System.Threading;
-using System.Threading.Tasks;
using GraphRag.Graphs;
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Linq;
@@ -11,22 +6,13 @@
namespace GraphRag.Storage.Cosmos;
-public sealed class CosmosGraphStore : IGraphStore
+public sealed class CosmosGraphStore(CosmosClient client, string databaseId, string nodesContainerId, string edgesContainerId, ILogger logger) : IGraphStore
{
- private readonly CosmosClient _client;
- private readonly string _databaseId;
- private readonly string _nodesContainerId;
- private readonly string _edgesContainerId;
- private readonly ILogger _logger;
-
- public CosmosGraphStore(CosmosClient client, string databaseId, string nodesContainerId, string edgesContainerId, ILogger logger)
- {
- _client = client ?? throw new ArgumentNullException(nameof(client));
- _databaseId = databaseId ?? throw new ArgumentNullException(nameof(databaseId));
- _nodesContainerId = nodesContainerId ?? throw new ArgumentNullException(nameof(nodesContainerId));
- _edgesContainerId = edgesContainerId ?? throw new ArgumentNullException(nameof(edgesContainerId));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
+ private readonly CosmosClient _client = client ?? throw new ArgumentNullException(nameof(client));
+ private readonly string _databaseId = databaseId ?? throw new ArgumentNullException(nameof(databaseId));
+ private readonly string _nodesContainerId = nodesContainerId ?? throw new ArgumentNullException(nameof(nodesContainerId));
+ private readonly string _edgesContainerId = edgesContainerId ?? throw new ArgumentNullException(nameof(edgesContainerId));
+ private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
@@ -60,7 +46,7 @@ public async Task UpsertRelationshipAsync(string sourceId, string targetId, stri
public IAsyncEnumerable GetOutgoingRelationshipsAsync(string sourceId, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(sourceId);
- return Fetch();
+ return Fetch(cancellationToken);
async IAsyncEnumerable Fetch([EnumeratorCancellation] CancellationToken token = default)
{
diff --git a/src/ManagedCode.GraphRag.CosmosDb/ServiceCollectionExtensions.cs b/src/ManagedCode.GraphRag.CosmosDb/ServiceCollectionExtensions.cs
index fa8b907b2f..3559459ca7 100644
--- a/src/ManagedCode.GraphRag.CosmosDb/ServiceCollectionExtensions.cs
+++ b/src/ManagedCode.GraphRag.CosmosDb/ServiceCollectionExtensions.cs
@@ -1,4 +1,3 @@
-using System;
using System.Text.Json;
using GraphRag.Graphs;
using Microsoft.Azure.Cosmos;
diff --git a/src/ManagedCode.GraphRag.CosmosDb/SystemTextJsonCosmosSerializer.cs b/src/ManagedCode.GraphRag.CosmosDb/SystemTextJsonCosmosSerializer.cs
index 0f57472fba..623304b9d6 100644
--- a/src/ManagedCode.GraphRag.CosmosDb/SystemTextJsonCosmosSerializer.cs
+++ b/src/ManagedCode.GraphRag.CosmosDb/SystemTextJsonCosmosSerializer.cs
@@ -1,11 +1,9 @@
-using System.IO;
-using System.Text;
using System.Text.Json;
using Microsoft.Azure.Cosmos;
namespace GraphRag.Storage.Cosmos;
-internal sealed class SystemTextJsonCosmosSerializer : CosmosSerializer
+internal sealed class SystemTextJsonCosmosSerializer(JsonSerializerOptions? options = null) : CosmosSerializer
{
private static readonly JsonSerializerOptions DefaultOptions = new(JsonSerializerDefaults.Web)
{
@@ -13,19 +11,11 @@ internal sealed class SystemTextJsonCosmosSerializer : CosmosSerializer
WriteIndented = false
};
- private readonly JsonSerializerOptions _options;
-
- public SystemTextJsonCosmosSerializer(JsonSerializerOptions? options = null)
- {
- _options = options ?? DefaultOptions;
- }
+ private readonly JsonSerializerOptions _options = options ?? DefaultOptions;
public override T FromStream(Stream stream)
{
- if (stream is null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
+ ArgumentNullException.ThrowIfNull(stream);
if (typeof(T) == typeof(Stream))
{
diff --git a/src/ManagedCode.GraphRag.Neo4j/Neo4jGraphStore.cs b/src/ManagedCode.GraphRag.Neo4j/Neo4jGraphStore.cs
index a28c3b380e..7de7fa03bb 100644
--- a/src/ManagedCode.GraphRag.Neo4j/Neo4jGraphStore.cs
+++ b/src/ManagedCode.GraphRag.Neo4j/Neo4jGraphStore.cs
@@ -1,31 +1,20 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Runtime.CompilerServices;
-using System.Threading;
-using System.Threading.Tasks;
using GraphRag.Graphs;
using Microsoft.Extensions.Logging;
using Neo4j.Driver;
namespace GraphRag.Storage.Neo4j;
-public sealed class Neo4jGraphStore : IGraphStore, IAsyncDisposable
+public sealed class Neo4jGraphStore(IDriver driver, ILogger logger) : IGraphStore, IAsyncDisposable
{
- private readonly IDriver _driver;
- private readonly ILogger _logger;
+ private readonly IDriver _driver = driver ?? throw new ArgumentNullException(nameof(driver));
+ private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
public Neo4jGraphStore(string uri, string username, string password, ILogger logger)
: this(GraphDatabase.Driver(uri, AuthTokens.Basic(username, password)), logger)
{
}
- public Neo4jGraphStore(IDriver driver, ILogger logger)
- {
- _driver = driver ?? throw new ArgumentNullException(nameof(driver));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -64,7 +53,7 @@ public IAsyncEnumerable GetOutgoingRelationshipsAsync(string
{
ArgumentException.ThrowIfNullOrWhiteSpace(sourceId);
cancellationToken.ThrowIfCancellationRequested();
- return Fetch();
+ return Fetch(cancellationToken);
async IAsyncEnumerable Fetch([EnumeratorCancellation] CancellationToken token = default)
{
diff --git a/src/ManagedCode.GraphRag.Neo4j/ServiceCollectionExtensions.cs b/src/ManagedCode.GraphRag.Neo4j/ServiceCollectionExtensions.cs
index 72fca2b88b..8941cb421a 100644
--- a/src/ManagedCode.GraphRag.Neo4j/ServiceCollectionExtensions.cs
+++ b/src/ManagedCode.GraphRag.Neo4j/ServiceCollectionExtensions.cs
@@ -1,4 +1,3 @@
-using System;
using GraphRag.Graphs;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
diff --git a/src/ManagedCode.GraphRag.Postgres/PostgresGraphStore.cs b/src/ManagedCode.GraphRag.Postgres/PostgresGraphStore.cs
index 7050fbee51..21b771ecd6 100644
--- a/src/ManagedCode.GraphRag.Postgres/PostgresGraphStore.cs
+++ b/src/ManagedCode.GraphRag.Postgres/PostgresGraphStore.cs
@@ -37,8 +37,20 @@ public PostgresGraphStore(PostgresGraphStoreOptions options, ILogger 0)
{
@@ -121,9 +137,15 @@ public async Task UpsertRelationshipAsync(string sourceId, string targetId, stri
var propertyAssignments = BuildPropertyAssignments("rel", ConvertProperties(properties), parameters, "rel_prop");
var queryBuilder = new StringBuilder();
- queryBuilder.Append($"MATCH (source {{ id: ${CypherParameterNames.SourceId} }}), (target {{ id: ${CypherParameterNames.TargetId} }})");
+ queryBuilder.Append("MATCH (source { id: $");
+ queryBuilder.Append(CypherParameterNames.SourceId);
+ queryBuilder.Append(" }), (target { id: $");
+ queryBuilder.Append(CypherParameterNames.TargetId);
+ queryBuilder.Append(" })");
queryBuilder.AppendLine();
- queryBuilder.Append($"MERGE (source)-[rel:{EscapeLabel(type)}]->(target)");
+ queryBuilder.Append("MERGE (source)-[rel:");
+ queryBuilder.Append(EscapeLabel(type));
+ queryBuilder.Append("]->(target)");
if (propertyAssignments.Count > 0)
{
diff --git a/src/ManagedCode.GraphRag/Cache/MemoryPipelineCache.cs b/src/ManagedCode.GraphRag/Cache/MemoryPipelineCache.cs
new file mode 100644
index 0000000000..31b97d56ab
--- /dev/null
+++ b/src/ManagedCode.GraphRag/Cache/MemoryPipelineCache.cs
@@ -0,0 +1,103 @@
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace GraphRag.Cache;
+
+///
+/// implementation backed by .
+///
+public sealed class MemoryPipelineCache : IPipelineCache
+{
+ private readonly IMemoryCache _memoryCache;
+ private readonly string _scope;
+ private readonly ConcurrentDictionary _keys;
+
+ public MemoryPipelineCache(IMemoryCache memoryCache)
+ : this(memoryCache, Guid.NewGuid().ToString("N"), new ConcurrentDictionary())
+ {
+ }
+
+ private MemoryPipelineCache(IMemoryCache memoryCache, string scope, ConcurrentDictionary keys)
+ {
+ _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
+ _scope = scope;
+ _keys = keys;
+ }
+
+ public Task