diff --git a/.changeset/stale-scissors-turn.md b/.changeset/stale-scissors-turn.md
new file mode 100644
index 0000000000000000000000000000000000000000..f3a320a0ff4994fd8b1737b24b5cbe9bb798a833
--- /dev/null
+++ b/.changeset/stale-scissors-turn.md
@@ -0,0 +1,5 @@
+---
+"create-llama": patch
+---
+
+Add contract review use case (Python)
diff --git a/e2e/shared/extractor_template.spec.ts b/e2e/shared/extractor_template.spec.ts
deleted file mode 100644
index 698d80527be6a67f54f80f819ea573d35358b127..0000000000000000000000000000000000000000
--- a/e2e/shared/extractor_template.spec.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/* eslint-disable turbo/no-undeclared-env-vars */
-import { expect, test } from "@playwright/test";
-import { ChildProcess } from "child_process";
-import fs from "fs";
-import path from "path";
-import { TemplateFramework } from "../../helpers";
-import { createTestDir, runCreateLlama } from "../utils";
-
-const templateFramework: TemplateFramework = process.env.FRAMEWORK
-  ? (process.env.FRAMEWORK as TemplateFramework)
-  : "fastapi";
-const dataSource: string = process.env.DATASOURCE
-  ? process.env.DATASOURCE
-  : "--example-file";
-
-// The extractor template currently only works with FastAPI and files (and not on Windows)
-if (
-  process.platform !== "win32" &&
-  templateFramework === "fastapi" &&
-  dataSource === "--example-file"
-) {
-  test.describe("Test extractor template", async () => {
-    let appPort: number;
-    let name: string;
-    let appProcess: ChildProcess;
-    let cwd: string;
-
-    // Create extractor app
-    test.beforeAll(async () => {
-      cwd = await createTestDir();
-      appPort = Math.floor(Math.random() * 10000) + 10000;
-      const result = await runCreateLlama({
-        cwd,
-        templateType: "extractor",
-        templateFramework: "fastapi",
-        dataSource: "--example-file",
-        vectorDb: "none",
-        port: appPort,
-        postInstallAction: "runApp",
-      });
-      name = result.projectName;
-      appProcess = result.appProcess;
-    });
-
-    test.afterAll(async () => {
-      appProcess.kill();
-    });
-
-    test("App folder should exist", async () => {
-      const dirExists = fs.existsSync(path.join(cwd, name));
-      expect(dirExists).toBeTruthy();
-    });
-    test("Frontend should have a title", async ({ page }) => {
-      await page.goto(`http://localhost:${appPort}`);
-      await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
-        timeout: 2000 * 60,
-      });
-    });
-  });
-}
diff --git a/e2e/shared/reflex_template.spec.ts b/e2e/shared/reflex_template.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..766d20a1f2dff3cb57f2388bd1bfdfdbf3f6edff
--- /dev/null
+++ b/e2e/shared/reflex_template.spec.ts
@@ -0,0 +1,64 @@
+/* eslint-disable turbo/no-undeclared-env-vars */
+import { expect, test } from "@playwright/test";
+import { ChildProcess } from "child_process";
+import fs from "fs";
+import path from "path";
+import { TemplateAgents, TemplateFramework } from "../../helpers";
+import { createTestDir, runCreateLlama } from "../utils";
+
+const templateFramework: TemplateFramework = process.env.FRAMEWORK
+  ? (process.env.FRAMEWORK as TemplateFramework)
+  : "fastapi";
+const dataSource: string = process.env.DATASOURCE
+  ? process.env.DATASOURCE
+  : "--example-file";
+const templateAgents: TemplateAgents[] = ["extractor", "contract_review"];
+
+// The reflex template currently only works with FastAPI and files (and not on Windows)
+if (
+  process.platform !== "win32" &&
+  templateFramework === "fastapi" &&
+  dataSource === "--example-file"
+) {
+  for (const agents of templateAgents) {
+    test.describe(`Test reflex template ${agents} ${templateFramework} ${dataSource}`, async () => {
+      let appPort: number;
+      let name: string;
+      let appProcess: ChildProcess;
+      let cwd: string;
+
+      // Create reflex app
+      test.beforeAll(async () => {
+        cwd = await createTestDir();
+        appPort = Math.floor(Math.random() * 10000) + 10000;
+        const result = await runCreateLlama({
+          cwd,
+          templateType: "reflex",
+          templateFramework: "fastapi",
+          dataSource: "--example-file",
+          vectorDb: "none",
+          port: appPort,
+          postInstallAction: "runApp",
+          agents,
+        });
+        name = result.projectName;
+        appProcess = result.appProcess;
+      });
+
+      test.afterAll(async () => {
+        appProcess.kill();
+      });
+
+      test("App folder should exist", async () => {
+        const dirExists = fs.existsSync(path.join(cwd, name));
+        expect(dirExists).toBeTruthy();
+      });
+      test("Frontend should have a title", async ({ page }) => {
+        await page.goto(`http://localhost:${appPort}`);
+        await expect(page.getByText("Built by LlamaIndex")).toBeVisible({
+          timeout: 2000 * 60,
+        });
+      });
+    });
+  }
+}
diff --git a/e2e/utils.ts b/e2e/utils.ts
index 4d1dd6283de09b781bcde5ed9b99283cc405a44b..e7a9cc9a3efbf077e3653ba94a054e41ae8c3bcc 100644
--- a/e2e/utils.ts
+++ b/e2e/utils.ts
@@ -113,7 +113,7 @@ export async function runCreateLlama({
   if (observability) {
     commandArgs.push("--observability", observability);
   }
-  if (templateType === "multiagent" && agents) {
+  if ((templateType === "multiagent" || templateType === "reflex") && agents) {
     commandArgs.push("--agents", agents);
   }
 
diff --git a/helpers/datasources.ts b/helpers/datasources.ts
index 80b936b0db3cb914bcff2709cadefce3626c7ed8..f2ef13e2fd9c5043a5cb1f352f4753116ded4a2b 100644
--- a/helpers/datasources.ts
+++ b/helpers/datasources.ts
@@ -18,6 +18,7 @@ export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [
       url: new URL(
         "https://s2.q4cdn.com/470004039/files/doc_earnings/2023/q4/filing/_10-K-Q4-2023-As-Filed.pdf",
       ),
+      filename: "apple_10k_report.pdf",
     },
   },
   {
@@ -26,10 +27,21 @@ export const EXAMPLE_10K_SEC_FILES: TemplateDataSource[] = [
       url: new URL(
         "https://ir.tesla.com/_flysystem/s3/sec/000162828024002390/tsla-20231231-gen.pdf",
       ),
+      filename: "tesla_10k_report.pdf",
     },
   },
 ];
 
+export const EXAMPLE_GDPR: TemplateDataSource = {
+  type: "file",
+  config: {
+    url: new URL(
+      "https://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32016R0679",
+    ),
+    filename: "gdpr.pdf",
+  },
+};
+
 export function getDataSources(
   files?: string,
   exampleFile?: boolean,
diff --git a/helpers/index.ts b/helpers/index.ts
index 27bdd32e2a7b3139e347fa041179b3fea52bfc05..40f73a8b62a47a11732fba7b8f38be5646d477b8 100644
--- a/helpers/index.ts
+++ b/helpers/index.ts
@@ -118,7 +118,8 @@ const prepareContextData = async (
       const destPath = path.join(
         root,
         "data",
-        path.basename(dataSourceConfig.url.toString()),
+        dataSourceConfig.filename ??
+          path.basename(dataSourceConfig.url.toString()),
       );
       await downloadFile(dataSourceConfig.url.toString(), destPath);
     } else {
@@ -192,7 +193,7 @@ export const installTemplate = async (
     if (
       props.template === "streaming" ||
       props.template === "multiagent" ||
-      props.template === "extractor"
+      props.template === "reflex"
     ) {
       await createBackendEnvFile(props.root, props);
     }
diff --git a/helpers/python.ts b/helpers/python.ts
index 5e08008b48a44a9f939d080256218cb8b081fa98..e1b2271f095f94edc1602f7c4dbf947ff85669da 100644
--- a/helpers/python.ts
+++ b/helpers/python.ts
@@ -405,8 +405,8 @@ export const installPythonTemplate = async ({
 >) => {
   console.log("\nInitializing Python project with template:", template, "\n");
   let templatePath;
-  if (template === "extractor") {
-    templatePath = path.join(templatesDir, "types", "extractor", framework);
+  if (template === "reflex") {
+    templatePath = path.join(templatesDir, "types", "reflex");
   } else {
     templatePath = path.join(templatesDir, "types", "streaming", framework);
   }
@@ -472,24 +472,6 @@ export const installPythonTemplate = async ({
       cwd: path.join(compPath, "engines", "python", engine),
     });
 
-    // Copy agent code
-    if (template === "multiagent") {
-      if (agents) {
-        await copy("**", path.join(root), {
-          parents: true,
-          cwd: path.join(compPath, "agents", "python", agents),
-          rename: assetRelocator,
-        });
-      } else {
-        console.log(
-          red(
-            "There is no agent selected for multi-agent template. Please pick an agent to use via --agents flag.",
-          ),
-        );
-        process.exit(1);
-      }
-    }
-
     // Copy router code
     await copyRouterCode(root, tools ?? []);
   }
@@ -503,6 +485,28 @@ export const installPythonTemplate = async ({
     });
   }
 
+  if (template === "multiagent" || template === "reflex") {
+    if (agents) {
+      const sourcePath =
+        template === "multiagent"
+          ? path.join(compPath, "agents", "python", agents)
+          : path.join(compPath, "reflex", agents);
+
+      await copy("**", path.join(root), {
+        parents: true,
+        cwd: sourcePath,
+        rename: assetRelocator,
+      });
+    } else {
+      console.log(
+        red(
+          `There is no agent selected for ${template} template. Please pick an agent to use via --agents flag.`,
+        ),
+      );
+      process.exit(1);
+    }
+  }
+
   console.log("Adding additional dependencies");
 
   const addOnDependencies = getAdditionalDependencies(
diff --git a/helpers/run-app.ts b/helpers/run-app.ts
index 991a9790d93e995643a834f58a91f26a624d7cf3..93787a8cbf2a87558ddfa40f9970a0c6033acfd7 100644
--- a/helpers/run-app.ts
+++ b/helpers/run-app.ts
@@ -1,5 +1,5 @@
 import { SpawnOptions, spawn } from "child_process";
-import { TemplateFramework } from "./types";
+import { TemplateFramework, TemplateType } from "./types";
 
 const createProcess = (
   command: string,
@@ -58,17 +58,17 @@ export function runTSApp(appPath: string, port: number) {
 
 export async function runApp(
   appPath: string,
-  template: string,
+  template: TemplateType,
   framework: TemplateFramework,
   port?: number,
 ): Promise<void> {
   try {
     // Start the app
     const defaultPort =
-      framework === "nextjs" || template === "extractor" ? 3000 : 8000;
+      framework === "nextjs" || template === "reflex" ? 3000 : 8000;
 
     const appRunner =
-      template === "extractor"
+      template === "reflex"
         ? runReflexApp
         : framework === "fastapi"
           ? runFastAPIApp
diff --git a/helpers/types.ts b/helpers/types.ts
index 75fdc60d6d18cb7cd151f0feea3425adf6d95df7..a4635f0e2736815cb172324281f921827f1478a6 100644
--- a/helpers/types.ts
+++ b/helpers/types.ts
@@ -20,11 +20,11 @@ export type ModelConfig = {
   isConfigured(): boolean;
 };
 export type TemplateType =
-  | "extractor"
   | "streaming"
   | "community"
   | "llamapack"
-  | "multiagent";
+  | "multiagent"
+  | "reflex";
 export type TemplateFramework = "nextjs" | "express" | "fastapi";
 export type TemplateUI = "html" | "shadcn";
 export type TemplateVectorDB =
@@ -49,14 +49,21 @@ export type TemplateDataSource = {
 };
 export type TemplateDataSourceType = "file" | "web" | "db";
 export type TemplateObservability = "none" | "traceloop" | "llamatrace";
-export type TemplateAgents = "financial_report" | "blog" | "form_filling";
+export type TemplateAgents =
+  | "financial_report"
+  | "blog"
+  | "form_filling"
+  | "extractor"
+  | "contract_review";
 // Config for both file and folder
 export type FileSourceConfig =
   | {
       path: string;
+      filename?: string;
     }
   | {
       url: URL;
+      filename?: string;
     };
 export type WebSourceConfig = {
   baseUrl?: string;
diff --git a/helpers/typescript.ts b/helpers/typescript.ts
index 3c57ba3bb27745acdb0f60912d35fd9b5fbb4389..761a4bb3d88e111426c4709e4c2d0b0209339856 100644
--- a/helpers/typescript.ts
+++ b/helpers/typescript.ts
@@ -153,7 +153,7 @@ export const installTSTemplate = async ({
     } else {
       console.log(
         red(
-          "There is no agent selected for multi-agent template. Please pick an agent to use via --agents flag.",
+          `There is no agent selected for ${template} template. Please pick an agent to use via --agents flag.`,
         ),
       );
       process.exit(1);
diff --git a/index.ts b/index.ts
index 1cbe50de18c925b00ef7601c45789a00c17e1d84..4a3438995125fba565e32ba9feceedec9132d2bb 100644
--- a/index.ts
+++ b/index.ts
@@ -215,7 +215,7 @@ const options = program.opts();
 
 if (
   process.argv.includes("--no-llama-parse") ||
-  options.template === "extractor"
+  options.template === "reflex"
 ) {
   options.useLlamaParse = false;
 }
diff --git a/questions/datasources.ts b/questions/datasources.ts
index db282af98e4a0d31351ed2d5e6a5748cfd61822f..1961e4c883414f981a9a387b95c60c23388d49a7 100644
--- a/questions/datasources.ts
+++ b/questions/datasources.ts
@@ -49,7 +49,7 @@ export const getDataSourceChoices = (
     );
   }
 
-  if (framework === "fastapi" && template !== "extractor") {
+  if (framework === "fastapi" && template !== "reflex") {
     choices.push({
       title: "Use website content (requires Chrome)",
       value: "web",
diff --git a/questions/questions.ts b/questions/questions.ts
index 2427b70a697c7975eea43d878c20aa4ec6b58249..ebc83396f52f30674401fc491e3b26609bfaeeef 100644
--- a/questions/questions.ts
+++ b/questions/questions.ts
@@ -95,8 +95,8 @@ export const askProQuestions = async (program: QuestionArgs) => {
     return; // early return - no further questions needed for llamapack projects
   }
 
-  if (program.template === "extractor") {
-    // Extractor template only supports FastAPI, empty data sources, and llamacloud
+  if (program.template === "reflex") {
+    // Reflex template only supports FastAPI, empty data sources, and llamacloud
     // So we just use example file for extractor template, this allows user to choose vector database later
     program.dataSources = [EXAMPLE_FILE];
     program.framework = "fastapi";
@@ -354,11 +354,8 @@ export const askProQuestions = async (program: QuestionArgs) => {
     // default to use LlamaParse if using LlamaCloud
     program.useLlamaParse = true;
   } else {
-    // Extractor template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
-    if (
-      program.useLlamaParse === undefined &&
-      program.template !== "extractor"
-    ) {
+    // Reflex template doesn't support LlamaParse and LlamaCloud right now (cannot use asyncio loop in Reflex)
+    if (program.useLlamaParse === undefined && program.template !== "reflex") {
       // if already set useLlamaParse, don't ask again
       if (program.dataSources.some((ds) => ds.type === "file")) {
         const { useLlamaParse } = await prompts(
diff --git a/questions/simple.ts b/questions/simple.ts
index e7892acdd4e0cd33df7677f4314d30ea77e09339..198261984f3d4aab5eaea24cc001ea954e12d946 100644
--- a/questions/simple.ts
+++ b/questions/simple.ts
@@ -1,5 +1,9 @@
 import prompts from "prompts";
-import { EXAMPLE_10K_SEC_FILES, EXAMPLE_FILE } from "../helpers/datasources";
+import {
+  EXAMPLE_10K_SEC_FILES,
+  EXAMPLE_FILE,
+  EXAMPLE_GDPR,
+} from "../helpers/datasources";
 import { askModelConfig } from "../helpers/providers";
 import { getTools } from "../helpers/tools";
 import { ModelConfig, TemplateFramework } from "../helpers/types";
@@ -12,6 +16,7 @@ type AppType =
   | "financial_report_agent"
   | "form_filling"
   | "extractor"
+  | "contract_review"
   | "data_scientist";
 
 type SimpleAnswers = {
@@ -42,6 +47,10 @@ export const askSimpleQuestions = async (
         },
         { title: "Code Artifact Agent", value: "code_artifact" },
         { title: "Information Extractor", value: "extractor" },
+        {
+          title: "Contract Review (using Workflows)",
+          value: "contract_review",
+        },
       ],
     },
     questionHandlers,
@@ -51,7 +60,7 @@ export const askSimpleQuestions = async (
   let llamaCloudKey = args.llamaCloudKey;
   let useLlamaCloud = false;
 
-  if (appType !== "extractor") {
+  if (appType !== "extractor" && appType !== "contract_review") {
     const { language: newLanguage } = await prompts(
       {
         type: "select",
@@ -166,11 +175,19 @@ const convertAnswers = async (
       modelConfig: MODEL_GPT4o,
     },
     extractor: {
-      template: "extractor",
+      template: "reflex",
+      agents: "extractor",
       tools: [],
       frontend: false,
       dataSources: [EXAMPLE_FILE],
     },
+    contract_review: {
+      template: "reflex",
+      agents: "contract_review",
+      tools: [],
+      frontend: false,
+      dataSources: [EXAMPLE_GDPR],
+    },
   };
   const results = lookup[answers.appType];
   return {
diff --git a/templates/components/reflex/contract_review/README-template.md b/templates/components/reflex/contract_review/README-template.md
new file mode 100644
index 0000000000000000000000000000000000000000..27c762cd623b7d21ac6ccc11221ba30f86cd4fde
--- /dev/null
+++ b/templates/components/reflex/contract_review/README-template.md
@@ -0,0 +1,59 @@
+This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Reflex](https://reflex.dev/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama) featuring automated contract review and compliance analysis use case.
+
+## Getting Started
+
+First, setup the environment with poetry:
+
+> **_Note:_** This step is not needed if you are using the dev-container.
+
+```shell
+poetry install
+```
+
+Then check the parameters that have been pre-configured in the `.env` file in this directory. (E.g. you might need to configure an `OPENAI_API_KEY` if you're using OpenAI as model provider).
+
+Second, generate the embeddings of the example document in the `./data` directory:
+
+```shell
+poetry run generate
+```
+
+Third, start app with `reflex` command:
+
+```shell
+poetry run reflex run
+```
+
+To deploy the application, refer to the Reflex deployment guide: https://reflex.dev/docs/hosting/deploy-quick-start/
+
+### UI
+
+The application provides an interactive web interface accessible at http://localhost:3000 for testing the contract review workflow.
+
+To get started:
+
+1. Upload a contract document:
+
+   - Use the provided [example_vendor_agreement.md](./example_vendor_agreement.md) for testing
+   - Or upload your own document (supported formats: PDF, TXT, Markdown, DOCX)
+
+2. Review Process:
+   - The system will automatically analyze your document against compliance guidelines
+   - By default, it uses [GDPR](./data/gdpr.pdf) as the compliance benchmark
+   - Custom guidelines can be used by adding your policy documents to the `./data` directory and running `poetry run generate` to update the embeddings
+
+The interface will display the analysis results for the compliance of the contract document.
+
+### Development
+
+You can start editing the backend workflow by modifying the [`ContractReviewWorkflow`](./app/services/contract_reviewer.py).
+
+For UI, you can start looking at the [`AppState`](./app/ui/states/app.py) code and navigating to the appropriate components.
+
+## Learn More
+
+To learn more about LlamaIndex, take a look at the following resources:
+
+- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex.
+
+You can check out [the LlamaIndex GitHub repository](https://github.com/run-llama/llama_index) - your feedback and contributions are welcome!
diff --git a/templates/components/reflex/contract_review/app/config.py b/templates/components/reflex/contract_review/app/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e1be9930170c95727c6022b545195b9d362485f
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/config.py
@@ -0,0 +1,47 @@
+DATA_DIR = "data"
+UPLOADED_DIR = "output/uploaded"
+
+# Workflow prompts
+CONTRACT_EXTRACT_PROMPT = """\
+You are given contract data below. \
+Please extract out relevant information from the contract into the defined schema - the schema is defined as a function call.\
+
+{contract_data}
+"""
+
+CONTRACT_MATCH_PROMPT = """\
+Given the following contract clause and the corresponding relevant guideline text, evaluate the compliance \
+and provide a JSON object that matches the ClauseComplianceCheck schema.
+
+**Contract Clause:**
+{clause_text}
+
+**Matched Guideline Text(s):**
+{guideline_text}
+"""
+
+
+COMPLIANCE_REPORT_SYSTEM_PROMPT = """\
+You are a compliance reporting assistant. Your task is to generate a final compliance report \
+based on the results of clause compliance checks against \
+a given set of guidelines. 
+
+Analyze the provided compliance results and produce a structured report according to the specified schema. 
+Ensure that if there are no noncompliant clauses, the report clearly indicates full compliance.
+"""
+
+COMPLIANCE_REPORT_USER_PROMPT = """\
+A set of clauses within a contract were checked against GDPR compliance guidelines for the following vendor: {vendor_name}. 
+The set of noncompliant clauses are given below.
+
+Each section includes:
+- **Clause:** The exact text of the contract clause.
+- **Guideline:** The relevant GDPR guideline text.
+- **Compliance Status:** Should be `False` for noncompliant clauses.
+- **Notes:** Additional information or explanations.
+
+{compliance_results}
+
+Based on the above compliance results, generate a final compliance report following the `ComplianceReport` schema below. 
+If there are no noncompliant clauses, the report should indicate that the contract is fully compliant.
+"""
diff --git a/templates/components/reflex/contract_review/app/models.py b/templates/components/reflex/contract_review/app/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..136b82bf97cd819e2bf8775a272ca50283123e1c
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/models.py
@@ -0,0 +1,85 @@
+from typing import List, Optional
+
+from pydantic import BaseModel, Field
+
+
+class ContractClause(BaseModel):
+    clause_text: str = Field(..., description="The exact text of the clause.")
+    mentions_data_processing: bool = Field(
+        False,
+        description="True if the clause involves personal data collection or usage.",
+    )
+    mentions_data_transfer: bool = Field(
+        False,
+        description="True if the clause involves transferring personal data, especially to third parties or across borders.",
+    )
+    requires_consent: bool = Field(
+        False,
+        description="True if the clause explicitly states that user consent is needed for data activities.",
+    )
+    specifies_purpose: bool = Field(
+        False,
+        description="True if the clause specifies a clear purpose for data handling or transfer.",
+    )
+    mentions_safeguards: bool = Field(
+        False,
+        description="True if the clause mentions security measures or other safeguards for data.",
+    )
+
+
+class ContractExtraction(BaseModel):
+    vendor_name: Optional[str] = Field(
+        None, description="The vendor's name if identifiable."
+    )
+    effective_date: Optional[str] = Field(
+        None, description="The effective date of the agreement, if available."
+    )
+    governing_law: Optional[str] = Field(
+        None, description="The governing law of the contract, if stated."
+    )
+    clauses: List[ContractClause] = Field(
+        ..., description="List of extracted clauses and their relevant indicators."
+    )
+
+
+class GuidelineMatch(BaseModel):
+    guideline_text: str = Field(
+        ...,
+        description="The single most relevant guideline excerpt related to this clause.",
+    )
+    similarity_score: float = Field(
+        ...,
+        description="Similarity score indicating how closely the guideline matches the clause, e.g., between 0 and 1.",
+    )
+    relevance_explanation: Optional[str] = Field(
+        None, description="Brief explanation of why this guideline is relevant."
+    )
+
+
+class ClauseComplianceCheck(BaseModel):
+    clause_text: str = Field(
+        ..., description="The exact text of the clause from the contract."
+    )
+    matched_guideline: Optional[GuidelineMatch] = Field(
+        None, description="The most relevant guideline extracted via vector retrieval."
+    )
+    compliant: bool = Field(
+        ...,
+        description="Indicates whether the clause is considered compliant with the referenced guideline.",
+    )
+    notes: Optional[str] = Field(
+        None, description="Additional commentary or recommendations."
+    )
+
+
+class ComplianceReport(BaseModel):
+    vendor_name: Optional[str] = Field(
+        None, description="The vendor's name if identified from the contract."
+    )
+    overall_compliant: bool = Field(
+        ..., description="Indicates if the contract is considered overall compliant."
+    )
+    summary_notes: str = Field(
+        ...,
+        description="Always give a general summary or recommendations for achieving full compliance.",
+    )
diff --git a/templates/components/reflex/contract_review/app/services/contract_reviewer.py b/templates/components/reflex/contract_review/app/services/contract_reviewer.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf16a83598fbcd7f6d390528c2da18d08824559a
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/services/contract_reviewer.py
@@ -0,0 +1,361 @@
+import logging
+import os
+import uuid
+from enum import Enum
+from pathlib import Path
+from typing import List
+
+from llama_index.core import SimpleDirectoryReader
+from llama_index.core.llms import LLM
+from llama_index.core.prompts import ChatPromptTemplate
+from llama_index.core.retrievers import BaseRetriever
+from llama_index.core.settings import Settings
+from llama_index.core.workflow import (
+    Context,
+    Event,
+    StartEvent,
+    StopEvent,
+    Workflow,
+    step,
+)
+
+from app.config import (
+    COMPLIANCE_REPORT_SYSTEM_PROMPT,
+    COMPLIANCE_REPORT_USER_PROMPT,
+    CONTRACT_EXTRACT_PROMPT,
+    CONTRACT_MATCH_PROMPT,
+)
+from app.engine.index import get_index
+from app.models import (
+    ClauseComplianceCheck,
+    ComplianceReport,
+    ContractClause,
+    ContractExtraction,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def get_workflow():
+    index = get_index()
+    if index is None:
+        raise RuntimeError(
+            "Index not found! Please run `poetry run generate` to populate an index first."
+        )
+    return ContractReviewWorkflow(
+        guideline_retriever=index.as_retriever(),
+        llm=Settings.llm,
+        verbose=True,
+        timeout=120,
+    )
+
+
+class Step(Enum):
+    PARSE_CONTRACT = "parse_contract"
+    ANALYZE_CLAUSES = "analyze_clauses"
+    HANDLE_CLAUSE = "handle_clause"
+    GENERATE_REPORT = "generate_report"
+
+
+class ContractExtractionEvent(Event):
+    contract_extraction: ContractExtraction
+
+
+class MatchGuidelineEvent(Event):
+    request_id: str
+    clause: ContractClause
+    vendor_name: str
+
+
+class MatchGuidelineResultEvent(Event):
+    result: ClauseComplianceCheck
+
+
+class GenerateReportEvent(Event):
+    match_results: List[ClauseComplianceCheck]
+
+
+class LogEvent(Event):
+    msg: str
+    step: Step
+    data: dict = {}
+    is_step_completed: bool = False
+
+
+class ContractReviewWorkflow(Workflow):
+    """Contract review workflow."""
+
+    def __init__(
+        self,
+        guideline_retriever: BaseRetriever,
+        llm: LLM | None = None,
+        similarity_top_k: int = 20,
+        **kwargs,
+    ) -> None:
+        """Init params."""
+        super().__init__(**kwargs)
+
+        self.guideline_retriever = guideline_retriever
+
+        self.llm = llm or Settings.llm
+        self.similarity_top_k = similarity_top_k
+
+        # if not exists, create
+        out_path = Path("output") / "workflow_output"
+        if not out_path.exists():
+            out_path.mkdir(parents=True, exist_ok=True)
+            os.chmod(str(out_path), 0o0777)
+        self.output_dir = out_path
+
+    @step
+    async def parse_contract(
+        self, ctx: Context, ev: StartEvent
+    ) -> ContractExtractionEvent:
+        """Parse the contract."""
+        uploaded_contract_path = Path(ev.contract_path)
+        contract_file_name = uploaded_contract_path.name
+        # Set contract file name in context
+        await ctx.set("contract_file_name", contract_file_name)
+
+        # Parse and read the contract to documents
+        docs = SimpleDirectoryReader(
+            input_files=[str(uploaded_contract_path)]
+        ).load_data()
+        ctx.write_event_to_stream(
+            LogEvent(
+                msg=f"Loaded document: {contract_file_name}",
+                step=Step.PARSE_CONTRACT,
+                data={
+                    "saved_path": str(uploaded_contract_path),
+                    "parsed_data": None,
+                },
+            )
+        )
+
+        # Parse the contract into a structured model
+        # See the ContractExtraction model for information we want to extract
+        ctx.write_event_to_stream(
+            LogEvent(
+                msg="Extracting information from the document",
+                step=Step.PARSE_CONTRACT,
+                data={
+                    "saved_path": str(uploaded_contract_path),
+                    "parsed_data": None,
+                },
+            )
+        )
+        prompt = ChatPromptTemplate.from_messages([("user", CONTRACT_EXTRACT_PROMPT)])
+        contract_extraction = await self.llm.astructured_predict(
+            ContractExtraction,
+            prompt,
+            contract_data="\n".join(
+                [d.get_content(metadata_mode="all") for d in docs]  # type: ignore
+            ),
+        )
+        if not isinstance(contract_extraction, ContractExtraction):
+            raise ValueError(f"Invalid extraction from contract: {contract_extraction}")
+
+        # save output template to file
+        contract_extraction_path = Path(f"{self.output_dir}/{contract_file_name}.json")
+        with open(contract_extraction_path, "w") as fp:
+            fp.write(contract_extraction.model_dump_json())
+
+        ctx.write_event_to_stream(
+            LogEvent(
+                msg="Extracted successfully",
+                step=Step.PARSE_CONTRACT,
+                is_step_completed=True,
+                data={
+                    "saved_path": str(contract_extraction_path),
+                    "parsed_data": contract_extraction.model_dump_json(),
+                },
+            )
+        )
+
+        return ContractExtractionEvent(contract_extraction=contract_extraction)
+
+    @step
+    async def dispatch_guideline_match(  # type: ignore
+        self, ctx: Context, ev: ContractExtractionEvent
+    ) -> MatchGuidelineEvent:
+        """For each clause in the contract, find relevant guidelines.
+
+        Use a map-reduce pattern, send each parsed clause as a MatchGuidelineEvent.
+        """
+        await ctx.set("num_clauses", len(ev.contract_extraction.clauses))
+        await ctx.set("vendor_name", ev.contract_extraction.vendor_name)
+
+        for clause in ev.contract_extraction.clauses:
+            request_id = str(uuid.uuid4())
+            ctx.send_event(
+                MatchGuidelineEvent(
+                    request_id=request_id,
+                    clause=clause,
+                    vendor_name=ev.contract_extraction.vendor_name or "Not identified",
+                )
+            )
+        ctx.write_event_to_stream(
+            LogEvent(
+                msg=f"Created {len(ev.contract_extraction.clauses)} tasks for analyzing with the guidelines",
+                step=Step.ANALYZE_CLAUSES,
+            )
+        )
+
+    @step
+    async def handle_guideline_match(
+        self, ctx: Context, ev: MatchGuidelineEvent
+    ) -> MatchGuidelineResultEvent:
+        """Handle matching clause against guideline."""
+        ctx.write_event_to_stream(
+            LogEvent(
+                msg=f"Handling clause for request {ev.request_id}",
+                step=Step.HANDLE_CLAUSE,
+                data={
+                    "request_id": ev.request_id,
+                    "clause_text": ev.clause.clause_text,
+                    "is_compliant": None,
+                },
+            )
+        )
+
+        # retrieve matching guideline
+        query = f"""\
+Find the relevant guideline from {ev.vendor_name} that aligns with the following contract clause:
+
+{ev.clause.clause_text}
+"""
+        guideline_docs = self.guideline_retriever.retrieve(query)
+        guideline_text = "\n\n".join([g.get_content() for g in guideline_docs])
+
+        # extract compliance from contract into a structured model
+        # see ClauseComplianceCheck model for the schema
+        prompt = ChatPromptTemplate.from_messages([("user", CONTRACT_MATCH_PROMPT)])
+        compliance_output = await self.llm.astructured_predict(
+            ClauseComplianceCheck,
+            prompt,
+            clause_text=ev.clause.model_dump_json(),
+            guideline_text=guideline_text,
+        )
+
+        if not isinstance(compliance_output, ClauseComplianceCheck):
+            raise ValueError(f"Invalid compliance check: {compliance_output}")
+
+        ctx.write_event_to_stream(
+            LogEvent(
+                msg=f"Completed compliance check for request {ev.request_id}",
+                step=Step.HANDLE_CLAUSE,
+                is_step_completed=True,
+                data={
+                    "request_id": ev.request_id,
+                    "clause_text": ev.clause.clause_text,
+                    "is_compliant": compliance_output.compliant,
+                    "result": compliance_output,
+                },
+            )
+        )
+
+        return MatchGuidelineResultEvent(result=compliance_output)
+
+    @step
+    async def gather_guideline_match(
+        self, ctx: Context, ev: MatchGuidelineResultEvent
+    ) -> GenerateReportEvent | None:
+        """Handle matching clause against guideline."""
+        num_clauses = await ctx.get("num_clauses")
+        events = ctx.collect_events(ev, [MatchGuidelineResultEvent] * num_clauses)
+        if events is None:
+            return None
+
+        match_results = [e.result for e in events]
+        # save match results
+        contract_file_name = await ctx.get("contract_file_name")
+        match_results_path = Path(
+            f"{self.output_dir}/match_results_{contract_file_name}.jsonl"
+        )
+        with open(match_results_path, "w") as fp:
+            for mr in match_results:
+                fp.write(mr.model_dump_json() + "\n")
+
+        ctx.write_event_to_stream(
+            LogEvent(
+                msg=f"Processed {len(match_results)} clauses",
+                step=Step.ANALYZE_CLAUSES,
+                is_step_completed=True,
+                data={"saved_path": str(match_results_path)},
+            )
+        )
+        return GenerateReportEvent(match_results=[e.result for e in events])
+
+    @step
+    async def generate_output(self, ctx: Context, ev: GenerateReportEvent) -> StopEvent:
+        ctx.write_event_to_stream(
+            LogEvent(
+                msg="Generating Compliance Report",
+                step=Step.GENERATE_REPORT,
+                data={"is_completed": False},
+            )
+        )
+
+        # if all clauses are compliant, return a compliant result
+        non_compliant_results = [r for r in ev.match_results if not r.compliant]
+
+        # generate compliance results string
+        result_tmpl = """
+1. **Clause**: {clause}
+2. **Guideline:** {guideline}
+3. **Compliance Status:** {compliance_status}
+4. **Notes:** {notes}
+"""
+        non_compliant_strings = []
+        for nr in non_compliant_results:
+            non_compliant_strings.append(
+                result_tmpl.format(
+                    clause=nr.clause_text,
+                    guideline=nr.matched_guideline.guideline_text
+                    if nr.matched_guideline is not None
+                    else "No relevant guideline found",
+                    compliance_status=nr.compliant,
+                    notes=nr.notes,
+                )
+            )
+        non_compliant_str = "\n\n".join(non_compliant_strings)
+
+        prompt = ChatPromptTemplate.from_messages(
+            [
+                ("system", COMPLIANCE_REPORT_SYSTEM_PROMPT),
+                ("user", COMPLIANCE_REPORT_USER_PROMPT),
+            ]
+        )
+        compliance_report = await self.llm.astructured_predict(
+            ComplianceReport,
+            prompt,
+            compliance_results=non_compliant_str,
+            vendor_name=await ctx.get("vendor_name"),
+        )
+
+        # Save compliance report to file
+        contract_file_name = await ctx.get("contract_file_name")
+        compliance_report_path = Path(
+            f"{self.output_dir}/report_{contract_file_name}.json"
+        )
+        with open(compliance_report_path, "w") as fp:
+            fp.write(compliance_report.model_dump_json())
+
+        ctx.write_event_to_stream(
+            LogEvent(
+                msg=f"Compliance report saved to {compliance_report_path}",
+                step=Step.GENERATE_REPORT,
+                is_step_completed=True,
+                data={
+                    "saved_path": str(compliance_report_path),
+                    "result": compliance_report,
+                },
+            )
+        )
+
+        return StopEvent(
+            result={
+                "report": compliance_report,
+                "non_compliant_results": non_compliant_results,
+            }
+        )
diff --git a/templates/components/reflex/contract_review/app/ui/components/__init__.py b/templates/components/reflex/contract_review/app/ui/components/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..beff74239fe71247717f12951b3b835e997f104f
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/components/__init__.py
@@ -0,0 +1,9 @@
+from .upload import upload_component
+from .workflow import guideline_component, load_contract_component, report_component
+
+__all__ = [
+    "upload_component",
+    "load_contract_component",
+    "guideline_component",
+    "report_component",
+]
diff --git a/templates/components/reflex/contract_review/app/ui/components/shared/__init__.py b/templates/components/reflex/contract_review/app/ui/components/shared/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..269d20324179709d91cda880320243fa55612187
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/components/shared/__init__.py
@@ -0,0 +1,5 @@
+from .card import card_component
+
+__all__ = [
+    "card_component",
+]
diff --git a/templates/components/reflex/contract_review/app/ui/components/shared/card.py b/templates/components/reflex/contract_review/app/ui/components/shared/card.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3e18ab79dd2f1a9ab7310528642444f6862f685
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/components/shared/card.py
@@ -0,0 +1,20 @@
+import reflex as rx
+
+
+def card_component(
+    title: str,
+    children: rx.Component,
+    show_loading: bool = False,
+) -> rx.Component:
+    return rx.card(
+        rx.hstack(
+            rx.cond(show_loading, rx.spinner(size="2")),
+            rx.text(title, size="4"),
+            align_items="center",
+            gap="2",
+        ),
+        rx.divider(orientation="horizontal"),
+        rx.container(children),
+        width="100%",
+        background_color="var(--gray-3)",
+    )
diff --git a/templates/components/reflex/contract_review/app/ui/components/upload.py b/templates/components/reflex/contract_review/app/ui/components/upload.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c355b021c901b856040fe03c58fdd858b7a8750
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/components/upload.py
@@ -0,0 +1,30 @@
+import reflex as rx
+
+from app.ui.components.shared import card_component
+from app.ui.states.app import AppState
+
+
+def upload_component() -> rx.Component:
+    return card_component(
+        title="Upload",
+        children=rx.container(
+            rx.vstack(
+                rx.upload(
+                    rx.vstack(
+                        rx.text("Drag and drop files here or click to select files"),
+                    ),
+                    on_drop=AppState.handle_upload(
+                        rx.upload_files(upload_id="upload1")
+                    ),
+                    id="upload1",
+                    border="1px dotted rgb(107,99,246)",
+                    padding="1rem",
+                ),
+                rx.cond(
+                    AppState.uploaded_file != None,  # noqa: E711
+                    rx.text(AppState.uploaded_file.file_name),  # type: ignore
+                    rx.text("No file uploaded"),
+                ),
+            ),
+        ),
+    )
diff --git a/templates/components/reflex/contract_review/app/ui/components/workflow/__init__.py b/templates/components/reflex/contract_review/app/ui/components/workflow/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..be1f7b8c245aa12a01340221e5ba9a2b44b5ef07
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/components/workflow/__init__.py
@@ -0,0 +1,9 @@
+from .guideline import guideline_component
+from .load import load_contract_component
+from .report import report_component
+
+__all__ = [
+    "guideline_component",
+    "load_contract_component",
+    "report_component",
+]
diff --git a/templates/components/reflex/contract_review/app/ui/components/workflow/guideline.py b/templates/components/reflex/contract_review/app/ui/components/workflow/guideline.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce0308f5a42ebf363ab936aef7d049a30d0e2629
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/components/workflow/guideline.py
@@ -0,0 +1,113 @@
+from typing import List
+
+import reflex as rx
+
+from app.models import ClauseComplianceCheck
+from app.ui.components.shared import card_component
+from app.ui.states.workflow import GuidelineData, GuidelineHandlerState, GuidelineState
+
+
+def guideline_handler_component(item: List) -> rx.Component:
+    _id: str = item[0]
+    status: GuidelineData = item[1]
+
+    return rx.hover_card.root(
+        rx.hover_card.trigger(
+            rx.card(
+                rx.stack(
+                    rx.container(
+                        rx.cond(
+                            ~status.is_completed,
+                            rx.spinner(size="1"),
+                            rx.cond(
+                                status.is_compliant,
+                                rx.icon(tag="check", color="green"),
+                                rx.icon(tag="x", color="red"),
+                            ),
+                        ),
+                    ),
+                    rx.flex(
+                        rx.text(status.clause_text, size="1"),
+                    ),
+                ),
+            ),
+        ),
+        rx.hover_card.content(
+            rx.cond(
+                status.is_completed,
+                guideline_result_component(status.result),  # type: ignore
+                rx.spinner(size="1"),
+            ),
+            side="right",
+        ),
+    )
+
+
+def guideline_result_component(result: ClauseComplianceCheck) -> rx.Component:
+    return rx.inset(
+        rx.table.root(
+            rx.table.header(
+                rx.table.row(
+                    rx.table.cell("Clause"),
+                    rx.table.cell("Guideline"),
+                ),
+            ),
+            rx.table.body(
+                rx.table.row(
+                    # rx.table.cell("Clause"),
+                    rx.table.cell(
+                        rx.text(result.clause_text, size="1"),
+                    ),  # type: ignore
+                    rx.table.cell(
+                        rx.text(result.matched_guideline.guideline_text, size="1"),  # type: ignore
+                    ),
+                ),
+            ),
+        ),
+        rx.container(
+            rx.cond(
+                result.compliant,
+                rx.text(
+                    result.notes,  # type: ignore
+                    size="2",
+                    color="green",
+                ),
+                rx.text(
+                    result.notes,  # type: ignore
+                    size="2",
+                    color="red",
+                ),
+            )
+        ),
+    )
+
+
+def guideline_component() -> rx.Component:
+    return rx.cond(
+        GuidelineState.is_started,
+        card_component(
+            title="Analyze the document with provided guidelines",
+            children=rx.vstack(
+                rx.vstack(
+                    rx.foreach(
+                        GuidelineState.log,
+                        lambda log: rx.box(
+                            rx.text(log["msg"]),
+                        ),
+                    ),
+                ),
+                rx.cond(
+                    GuidelineHandlerState.has_data(),  # type: ignore
+                    rx.grid(
+                        rx.foreach(
+                            GuidelineHandlerState.data,
+                            guideline_handler_component,
+                        ),
+                        columns="2",
+                        spacing="1",
+                    ),
+                ),
+            ),
+            show_loading=GuidelineState.is_running,
+        ),
+    )
diff --git a/templates/components/reflex/contract_review/app/ui/components/workflow/load.py b/templates/components/reflex/contract_review/app/ui/components/workflow/load.py
new file mode 100644
index 0000000000000000000000000000000000000000..38d33b110bc4fc44f03e46559d7f92db08a3cd3e
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/components/workflow/load.py
@@ -0,0 +1,22 @@
+import reflex as rx
+
+from app.ui.components.shared import card_component
+from app.ui.states.workflow import ContractLoaderState
+
+
+def load_contract_component() -> rx.Component:
+    return rx.cond(
+        ContractLoaderState.is_started,
+        card_component(
+            title="Parse contract",
+            children=rx.vstack(
+                rx.foreach(
+                    ContractLoaderState.log,
+                    lambda log: rx.box(
+                        rx.text(log["msg"]),
+                    ),
+                ),
+            ),
+            show_loading=ContractLoaderState.is_running,
+        ),
+    )
diff --git a/templates/components/reflex/contract_review/app/ui/components/workflow/report.py b/templates/components/reflex/contract_review/app/ui/components/workflow/report.py
new file mode 100644
index 0000000000000000000000000000000000000000..46406f49764db16882c335ece1317aafc7026f25
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/components/workflow/report.py
@@ -0,0 +1,48 @@
+import reflex as rx
+
+from app.ui.components.shared import card_component
+from app.ui.states.workflow import ReportState
+
+
+def report_component() -> rx.Component:
+    return rx.cond(
+        ReportState.is_running,
+        card_component(
+            title="Conclusion",
+            show_loading=~ReportState.is_completed,  # type: ignore
+            children=rx.cond(
+                ReportState.is_completed,
+                rx.vstack(
+                    rx.box(
+                        rx.inset(
+                            rx.table.root(
+                                rx.table.body(
+                                    rx.table.row(
+                                        rx.table.cell("Vendor"),
+                                        rx.table.cell(ReportState.result.vendor_name),  # type: ignore
+                                    ),
+                                    rx.table.row(
+                                        rx.table.cell("Overall Compliance"),
+                                        rx.table.cell(
+                                            rx.cond(
+                                                ReportState.result.overall_compliant,
+                                                rx.text("Compliant", color="green"),
+                                                rx.text("Non-compliant", color="red"),
+                                            )
+                                        ),
+                                    ),
+                                    rx.table.row(
+                                        rx.table.cell("Summary Notes"),
+                                        rx.table.cell(ReportState.result.summary_notes),  # type: ignore
+                                    ),
+                                ),
+                            ),
+                        )
+                    ),
+                ),
+                rx.vstack(
+                    rx.text("Analyzing compliance results for final conclusion..."),
+                ),
+            ),
+        ),
+    )
diff --git a/templates/types/extractor/fastapi/app/ui/pages/__init__.py b/templates/components/reflex/contract_review/app/ui/pages/__init__.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/pages/__init__.py
rename to templates/components/reflex/contract_review/app/ui/pages/__init__.py
diff --git a/templates/components/reflex/contract_review/app/ui/pages/index.py b/templates/components/reflex/contract_review/app/ui/pages/index.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b430b0ca1965ee5c39e6aacf301a4543d5024cf
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/pages/index.py
@@ -0,0 +1,46 @@
+import reflex as rx
+
+from app.ui.components import (
+    guideline_component,
+    load_contract_component,
+    report_component,
+    upload_component,
+)
+from app.ui.templates import template
+
+
+@template(
+    route="/",
+    title="Structure extractor",
+)
+def index() -> rx.Component:
+    """The main index page."""
+    return rx.vstack(
+        rx.vstack(
+            rx.heading("Built by LlamaIndex", size="6"),
+            rx.text(
+                "Upload a contract to start the review process.",
+            ),
+            background_color="var(--gray-3)",
+            align_items="left",
+            justify_content="left",
+            width="100%",
+            padding="1rem",
+        ),
+        rx.container(
+            rx.vstack(
+                # Upload
+                upload_component(),
+                # Workflow
+                rx.vstack(
+                    load_contract_component(),
+                    guideline_component(),
+                    report_component(),
+                    width="100%",
+                ),
+            ),
+            width="100%",
+            padding="1rem",
+        ),
+        width="100%",
+    )
diff --git a/templates/components/reflex/contract_review/app/ui/states/__init__.py b/templates/components/reflex/contract_review/app/ui/states/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..39865f4b400e3b31f2f3fd6d9d3b010c710183f5
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/states/__init__.py
@@ -0,0 +1,15 @@
+from .app import AppState
+from .workflow import (
+    ContractLoaderState,
+    GuidelineHandlerState,
+    GuidelineState,
+    ReportState,
+)
+
+__all__ = [
+    "AppState",
+    "ContractLoaderState",
+    "GuidelineHandlerState",
+    "GuidelineState",
+    "ReportState",
+]
diff --git a/templates/components/reflex/contract_review/app/ui/states/app.py b/templates/components/reflex/contract_review/app/ui/states/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..f04c367782f6e229fe5c608ca936bec41553a307
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/states/app.py
@@ -0,0 +1,94 @@
+import logging
+import os
+from typing import List, Optional
+
+import reflex as rx
+
+from app.config import UPLOADED_DIR
+from app.services.contract_reviewer import LogEvent, Step, get_workflow
+from app.ui.states.workflow import (
+    ContractLoaderState,
+    GuidelineHandlerState,
+    GuidelineState,
+    ReportState,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class UploadedFile(rx.Base):
+    file_name: str
+    size: int
+
+
+class AppState(rx.State):
+    """
+    Whole main state for the app.
+    Handle for file upload, trigger workflow and produce workflow events.
+    """
+
+    uploaded_file: Optional[UploadedFile] = None
+
+    @rx.event
+    async def handle_upload(self, files: List[rx.UploadFile]):
+        if len(files) > 1:
+            yield rx.toast.error(
+                "You can only upload one file at a time", position="top-center"
+            )
+            return
+        try:
+            file = files[0]
+            upload_data = await file.read()
+            outfile = os.path.join(UPLOADED_DIR, file.filename)
+            with open(outfile, "wb") as f:
+                f.write(upload_data)
+            self.uploaded_file = UploadedFile(
+                file_name=file.filename, size=len(upload_data)
+            )
+            yield AppState.reset_workflow
+            yield AppState.trigger_workflow
+        except Exception as e:
+            yield rx.toast.error(str(e), position="top-center")
+
+    @rx.event
+    def reset_workflow(self):
+        yield ContractLoaderState.reset_state
+        yield GuidelineState.reset_state
+        yield GuidelineHandlerState.reset_state
+        yield ReportState.reset_state
+
+    @rx.event(background=True)
+    async def trigger_workflow(self):
+        """
+        Trigger backend to start reviewing the contract in a loop.
+        Get the event from the loop and update the state.
+        """
+        if self.uploaded_file is None:
+            yield rx.toast.error("No file uploaded", position="top-center")
+        else:
+            uploaded_file_path = os.path.join(
+                UPLOADED_DIR, self.uploaded_file.file_name
+            )
+
+            try:
+                workflow = get_workflow()
+                handler = workflow.run(
+                    contract_path=uploaded_file_path,
+                )
+                async for event in handler.stream_events():
+                    if isinstance(event, LogEvent):
+                        match event.step:
+                            case Step.PARSE_CONTRACT:
+                                yield ContractLoaderState.add_log(event)
+                            case Step.ANALYZE_CLAUSES:
+                                yield GuidelineState.add_log(event)
+                            case Step.HANDLE_CLAUSE:
+                                yield GuidelineHandlerState.add_log(event)
+                            case Step.GENERATE_REPORT:
+                                yield ReportState.add_log(event)
+                # Wait for workflow completion and propagate any exceptions
+                _ = await handler
+            except Exception as e:
+                logger.error(f"Error in trigger_workflow: {e}")
+                yield rx.toast.error(str(e), position="top-center")
+                yield AppState.reset_workflow
diff --git a/templates/components/reflex/contract_review/app/ui/states/workflow.py b/templates/components/reflex/contract_review/app/ui/states/workflow.py
new file mode 100644
index 0000000000000000000000000000000000000000..efba290baf3682bcf2bb6c3f3f38fe32af89244f
--- /dev/null
+++ b/templates/components/reflex/contract_review/app/ui/states/workflow.py
@@ -0,0 +1,137 @@
+from typing import Any, Dict, List, Optional
+
+import reflex as rx
+from pydantic import BaseModel
+
+from app.models import ClauseComplianceCheck, ComplianceReport
+from app.services.contract_reviewer import LogEvent
+from rxconfig import config as rx_config
+
+
+class ContractLoaderState(rx.State):
+    is_running: bool = False
+    is_started: bool = False
+    log: List[Dict[str, Any]] = []
+
+    @rx.event
+    async def add_log(self, log: LogEvent):
+        if not self.is_started:
+            yield ContractLoaderState.start
+        self.log.append(log.model_dump())
+        if log.is_step_completed:
+            yield ContractLoaderState.stop
+
+    def has_log(self):
+        return len(self.log) > 0
+
+    @rx.event
+    def start(self):
+        self.is_running = True
+        self.is_started = True
+
+    @rx.event
+    def stop(self):
+        self.is_running = False
+
+    @rx.event
+    def reset_state(self):
+        self.is_running = False
+        self.is_started = False
+        self.log = []
+
+
+class GuidelineState(rx.State):
+    is_running: bool = False
+    is_started: bool = False
+    log: List[Dict[str, Any]] = []
+
+    @rx.event
+    def add_log(self, log: LogEvent):
+        if not self.is_started:
+            yield GuidelineState.start
+        self.log.append(log.model_dump())
+        if log.is_step_completed:
+            yield GuidelineState.stop
+
+    def has_log(self):
+        return len(self.log) > 0
+
+    @rx.event
+    def reset_state(self):
+        self.is_running = False
+        self.is_started = False
+        self.log = []
+
+    @rx.event
+    def start(self):
+        self.is_running = True
+        self.is_started = True
+
+    @rx.event
+    def stop(self):
+        self.is_running = False
+
+
+class GuidelineData(BaseModel):
+    is_completed: bool
+    is_compliant: Optional[bool]
+    clause_text: Optional[str]
+    result: Optional[ClauseComplianceCheck] = None
+
+
+class GuidelineHandlerState(rx.State):
+    data: Dict[str, GuidelineData] = {}
+
+    def has_data(self):
+        return len(self.data) > 0
+
+    @rx.event
+    def add_log(self, log: LogEvent):
+        _id = log.data.get("request_id")
+        if _id is None:
+            return
+        is_compliant = log.data.get("is_compliant", None)
+        self.data[_id] = GuidelineData(
+            is_completed=log.is_step_completed,
+            is_compliant=is_compliant,
+            clause_text=log.data.get("clause_text", None),
+            result=log.data.get("result", None),
+        )
+        if log.is_step_completed:
+            yield self.stop(request_id=_id)
+
+    @rx.event
+    def reset_state(self):
+        self.data = {}
+
+    @rx.event
+    def stop(self, request_id: str):
+        # Update the item in the data to be completed
+        self.data[request_id].is_completed = True
+
+
+class ReportState(rx.State):
+    is_running: bool = False
+    is_completed: bool = False
+    saved_path: str = ""
+    result: Optional[ComplianceReport] = None
+
+    @rx.var()
+    def download_url(self) -> str:
+        return f"{rx_config.api_url}/api/download/{self.saved_path}"
+
+    @rx.event
+    def add_log(self, log: LogEvent):
+        if not self.is_running:
+            self.is_running = True
+        if log.is_step_completed:
+            self.is_completed = True
+            self.saved_path = log.data.get("saved_path")
+            self.result = log.data.get("result")
+
+    @rx.event
+    def reset_state(self):
+        self.is_running = False
+        self.is_completed = False
+        self.saved_path = ""
+        self.result = None
diff --git a/templates/components/reflex/contract_review/example_vendor_agreement.md b/templates/components/reflex/contract_review/example_vendor_agreement.md
new file mode 100644
index 0000000000000000000000000000000000000000..0e5dac6fa201532cb91ca865d0bc588e386e4a8c
--- /dev/null
+++ b/templates/components/reflex/contract_review/example_vendor_agreement.md
@@ -0,0 +1,142 @@
+# ACME Vendor Agreement
+
+**Effective Date:** January 1, 2024
+
+## Parties:
+
+- **Client:** LlamaCo ("Client")
+- **Vendor:** ACME Office Supply, Inc. ("Vendor")
+
+## 1. Overview
+
+This Vendor Agreement ("Agreement") sets forth the terms and conditions under which ACME Office Supply, Inc. will provide office supplies, consumables, related goods ("Products"), and associated data processing services to LlamaCo.
+
+## 2. Definitions
+
+- **Personal Data:** Any information relating to an identified or identifiable natural person ('data subject').
+- **Processing:** Any operation performed on Personal Data, including collection, storage, modification, transfer, or deletion.
+- **Data Controller:** LlamaCo, who determines the purposes and means of processing Personal Data.
+- **Data Processor:** ACME Office Supply, Inc., who processes Personal Data on behalf of the Controller.
+
+## 3. Data Protection and Privacy
+
+### 3.1 Scope of Processing
+
+Vendor shall process Personal Data only:
+
+- To fulfill orders and manage deliveries
+- To provide customer support services
+- To maintain business records
+- To comply with legal obligations
+
+### 3.2 Data Subject Rights
+
+Vendor shall:
+
+- Respond to data subject requests within 30 days
+- Provide data in a structured, commonly used format
+- Implement measures to facilitate data portability
+- Assist with data subject rights requests at no additional cost
+
+### 3.3 Data Transfers and Storage
+
+- Vendor maintains primary data centers in the United States
+- Vendor may transfer data to any country where it maintains operations
+- No prior notification required for new data storage locations
+- Vendor will rely on its standard data transfer mechanisms
+- Data may be processed by staff operating outside the EEA
+
+### 3.4 Subprocessors
+
+- Vendor may engage subprocessors without prior Client approval
+- Subprocessors may be located in any jurisdiction globally
+- Notice of new subprocessors provided within 30 days of engagement
+- Client has no right to object to new subprocessors
+
+## 4. Security Measures
+
+### 4.1 Technical and Organizational Measures
+
+Vendor shall implement appropriate measures including:
+
+- Encryption of Personal Data in transit and at rest
+- Access controls and authentication
+- Regular security testing and assessments
+- Employee training on data protection
+- Incident response procedures
+
+### 4.2 Data Breaches
+
+Vendor shall:
+
+- Notify Client of any Personal Data breach within 72 hours
+- Provide details necessary to meet regulatory requirements
+- Cooperate with Client's breach investigation
+- Maintain records of all data breaches
+
+## 5. Data Retention
+
+### 5.1 Retention Period
+
+- Personal Data retained only as long as necessary
+- Standard retention period of 3 years after last transaction
+- Deletion of Personal Data upon written request
+- Backup copies retained for maximum of 6 months
+
+### 5.2 Termination
+
+Upon termination of services:
+
+- Return all Personal Data in standard format
+- Delete existing copies within 30 days
+- Provide written confirmation of deletion
+- Cease all processing activities
+
+## 6. Compliance and Audit
+
+### 6.1 Documentation
+
+Vendor shall maintain:
+
+- Records of all processing activities
+- Security measure documentation
+- Data transfer mechanisms
+- Subprocessor agreements
+
+### 6.2 Audits
+
+- Annual compliance audits permitted
+- 30 days notice required for audits
+- Vendor to provide necessary documentation
+- Client bears reasonable audit costs
+
+## 7. Liability and Indemnification
+
+### 7.1 Liability
+
+- Vendor liable for data protection violations
+- Reasonable compensation for damages
+- Coverage for regulatory fines where applicable
+- Joint liability as required by law
+
+## 8. Governing Law
+
+This Agreement shall be governed by the laws of Ireland, without regard to its conflict of laws principles.
+
+---
+
+IN WITNESS WHEREOF, the parties have executed this Agreement as of the Effective Date.
+
+**LlamaCo**
+
+By: **_
+Name: [Authorized Representative]  
+Title: [Title]  
+Date: _**
+
+**ACME Office Supply, Inc.**
+
+By: **_  
+Name: [Authorized Representative]  
+Title: [Title]  
+Date: _**
diff --git a/templates/components/reflex/contract_review/pyproject.toml b/templates/components/reflex/contract_review/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..b11ad1096079aef8778e76f570df29ac7d79fc07
--- /dev/null
+++ b/templates/components/reflex/contract_review/pyproject.toml
@@ -0,0 +1,44 @@
+[tool]
+[tool.poetry]
+name = "app"
+version = "0.1.0"
+description = ""
+authors = [ "Marcus Schiesser <mail@marcusschiesser.de>" ]
+readme = "README.md"
+
+[tool.poetry.scripts]
+generate = "app.engine.generate:generate_datasource"
+
+[tool.poetry.dependencies]
+python = "^3.11,<4.0"
+fastapi = "^0.109.1"
+python-dotenv = "^1.0.0"
+pydantic = "<2.10"
+llama-index = "^0.12.1"
+cachetools = "^5.3.3"
+reflex = "^0.6.2.post1"
+
+[tool.poetry.dependencies.uvicorn]
+extras = [ "standard" ]
+version = "^0.23.2"
+
+[tool.poetry.dependencies.docx2txt]
+version = "^0.8"
+
+[tool.poetry.dependencies.llama-index-llms-openai]
+version = "^0.3.2"
+
+[tool.poetry.dependencies.llama-index-embeddings-openai]
+version = "^0.3.1"
+
+[tool.poetry.dependencies.llama-index-agent-openai]
+version = "^0.4.0"
+
+
+[tool.poetry.group.dev.dependencies]
+pytest-asyncio = "^0.25.0"
+pytest = "^8.3.4"
+
+[build-system]
+requires = [ "poetry-core" ]
+build-backend = "poetry.core.masonry.api"
\ No newline at end of file
diff --git a/templates/types/extractor/fastapi/README-template.md b/templates/components/reflex/extractor/README-template.md
similarity index 100%
rename from templates/types/extractor/fastapi/README-template.md
rename to templates/components/reflex/extractor/README-template.md
diff --git a/templates/types/extractor/fastapi/app/__init__.py b/templates/components/reflex/extractor/app/api/__init__.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/__init__.py
rename to templates/components/reflex/extractor/app/api/__init__.py
diff --git a/templates/types/extractor/fastapi/app/api/models.py b/templates/components/reflex/extractor/app/api/models.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/api/models.py
rename to templates/components/reflex/extractor/app/api/models.py
diff --git a/templates/types/extractor/fastapi/app/api/__init__.py b/templates/components/reflex/extractor/app/api/routers/__init__.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/api/__init__.py
rename to templates/components/reflex/extractor/app/api/routers/__init__.py
diff --git a/templates/types/extractor/fastapi/app/api/routers/extractor.py b/templates/components/reflex/extractor/app/api/routers/extractor.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/api/routers/extractor.py
rename to templates/components/reflex/extractor/app/api/routers/extractor.py
diff --git a/templates/types/extractor/fastapi/app/api/routers/main.py b/templates/components/reflex/extractor/app/api/routers/main.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/api/routers/main.py
rename to templates/components/reflex/extractor/app/api/routers/main.py
diff --git a/templates/types/extractor/fastapi/app/models/output.py b/templates/components/reflex/extractor/app/models/output.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/models/output.py
rename to templates/components/reflex/extractor/app/models/output.py
diff --git a/templates/types/extractor/fastapi/app/services/extractor.py b/templates/components/reflex/extractor/app/services/extractor.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/services/extractor.py
rename to templates/components/reflex/extractor/app/services/extractor.py
diff --git a/templates/types/extractor/fastapi/app/services/model.py b/templates/components/reflex/extractor/app/services/model.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/services/model.py
rename to templates/components/reflex/extractor/app/services/model.py
diff --git a/templates/types/extractor/fastapi/app/ui/components/__init__.py b/templates/components/reflex/extractor/app/ui/components/__init__.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/components/__init__.py
rename to templates/components/reflex/extractor/app/ui/components/__init__.py
diff --git a/templates/types/extractor/fastapi/app/ui/components/extractor.py b/templates/components/reflex/extractor/app/ui/components/extractor.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/components/extractor.py
rename to templates/components/reflex/extractor/app/ui/components/extractor.py
diff --git a/templates/types/extractor/fastapi/app/ui/components/monaco.py b/templates/components/reflex/extractor/app/ui/components/monaco.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/components/monaco.py
rename to templates/components/reflex/extractor/app/ui/components/monaco.py
diff --git a/templates/types/extractor/fastapi/app/ui/components/schema_editor.py b/templates/components/reflex/extractor/app/ui/components/schema_editor.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/components/schema_editor.py
rename to templates/components/reflex/extractor/app/ui/components/schema_editor.py
diff --git a/templates/types/extractor/fastapi/app/ui/components/upload.py b/templates/components/reflex/extractor/app/ui/components/upload.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/components/upload.py
rename to templates/components/reflex/extractor/app/ui/components/upload.py
diff --git a/templates/components/reflex/extractor/app/ui/pages/__init__.py b/templates/components/reflex/extractor/app/ui/pages/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e27089cf6cff73073e6d2a40ece2eb18d5557f99
--- /dev/null
+++ b/templates/components/reflex/extractor/app/ui/pages/__init__.py
@@ -0,0 +1 @@
+from .index import index as index
diff --git a/templates/types/extractor/fastapi/app/ui/pages/index.py b/templates/components/reflex/extractor/app/ui/pages/index.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/pages/index.py
rename to templates/components/reflex/extractor/app/ui/pages/index.py
diff --git a/templates/types/extractor/fastapi/pyproject.toml b/templates/components/reflex/extractor/pyproject.toml
similarity index 100%
rename from templates/types/extractor/fastapi/pyproject.toml
rename to templates/components/reflex/extractor/pyproject.toml
diff --git a/templates/types/extractor/fastapi/app/api/routers/__init__.py b/templates/types/reflex/app/__init__.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/api/routers/__init__.py
rename to templates/types/reflex/app/__init__.py
diff --git a/templates/types/reflex/app/api/__init__.py b/templates/types/reflex/app/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/templates/types/reflex/app/api/routers/__init__.py b/templates/types/reflex/app/api/routers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/templates/types/reflex/app/api/routers/main.py b/templates/types/reflex/app/api/routers/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ac04111b0b5c430897abc5337c6eb65241d61fd
--- /dev/null
+++ b/templates/types/reflex/app/api/routers/main.py
@@ -0,0 +1,4 @@
+from fastapi import APIRouter
+
+
+api_router = APIRouter()
diff --git a/templates/types/extractor/fastapi/app/app.py b/templates/types/reflex/app/app.py
similarity index 54%
rename from templates/types/extractor/fastapi/app/app.py
rename to templates/types/reflex/app/app.py
index 5ca71cb28907fd7767dee92772a162f228ac4b94..a4b4d9570403f30ef683d7be7b2c02f3338f410c 100644
--- a/templates/types/extractor/fastapi/app/app.py
+++ b/templates/types/reflex/app/app.py
@@ -1,22 +1,18 @@
 # flake8: noqa: E402
+
 from dotenv import load_dotenv
 
 load_dotenv()
 
 
 import reflex as rx
-from fastapi import FastAPI
 
-from app.api.routers.extractor import extractor_router
+from app.api.routers.main import api_router
 from app.settings import init_settings
 from app.ui.pages import *  # Keep this import all pages in the app  # noqa: F403
 
 init_settings()
 
 
-def add_routers(app: FastAPI):
-    app.include_router(extractor_router, prefix="/api/extractor")
-
-
 app = rx.App()
-add_routers(app.api)
+app.api.include_router(api_router)
diff --git a/templates/types/extractor/fastapi/app/config.py b/templates/types/reflex/app/config.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/config.py
rename to templates/types/reflex/app/config.py
diff --git a/templates/types/extractor/fastapi/app/engine/__init__.py b/templates/types/reflex/app/engine/__init__.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/engine/__init__.py
rename to templates/types/reflex/app/engine/__init__.py
diff --git a/templates/types/extractor/fastapi/app/engine/engine.py b/templates/types/reflex/app/engine/engine.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/engine/engine.py
rename to templates/types/reflex/app/engine/engine.py
diff --git a/templates/types/extractor/fastapi/app/engine/generate.py b/templates/types/reflex/app/engine/generate.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/engine/generate.py
rename to templates/types/reflex/app/engine/generate.py
diff --git a/templates/types/extractor/fastapi/app/engine/index.py b/templates/types/reflex/app/engine/index.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/engine/index.py
rename to templates/types/reflex/app/engine/index.py
diff --git a/templates/types/extractor/fastapi/app/ui/templates/__init__.py b/templates/types/reflex/app/ui/templates/__init__.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/templates/__init__.py
rename to templates/types/reflex/app/ui/templates/__init__.py
diff --git a/templates/types/extractor/fastapi/app/ui/templates/styles.py b/templates/types/reflex/app/ui/templates/styles.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/templates/styles.py
rename to templates/types/reflex/app/ui/templates/styles.py
diff --git a/templates/types/extractor/fastapi/app/ui/templates/template.py b/templates/types/reflex/app/ui/templates/template.py
similarity index 100%
rename from templates/types/extractor/fastapi/app/ui/templates/template.py
rename to templates/types/reflex/app/ui/templates/template.py
diff --git a/templates/types/extractor/fastapi/gitignore b/templates/types/reflex/gitignore
similarity index 100%
rename from templates/types/extractor/fastapi/gitignore
rename to templates/types/reflex/gitignore
diff --git a/templates/types/extractor/fastapi/rxconfig.py b/templates/types/reflex/rxconfig.py
similarity index 100%
rename from templates/types/extractor/fastapi/rxconfig.py
rename to templates/types/reflex/rxconfig.py