add agent context

This commit is contained in:
Anibal Angulo
2025-11-09 08:35:01 -06:00
parent a23f45ca6d
commit 77a11ef32e
16 changed files with 1227 additions and 946 deletions

View File

@@ -11,15 +11,23 @@ from .models import (
async def build_audit_report(payload: dict[str, Any]) -> AuditReport:
extraction_payload = payload.get("extraction")
metadata_raw: Any = None
extraction_payload: Any = None
if isinstance(payload, dict) and "extraction" in payload:
extraction_payload = payload.get("extraction")
metadata_raw = payload.get("metadata")
else:
extraction_payload = payload
if extraction_payload is None:
raise ValueError("Payload missing 'extraction' key.")
raise ValueError("Payload missing extraction data.")
extraction = ExtractedIrsForm990PfDataSchema.model_validate(extraction_payload)
initial_findings = prepare_initial_findings(extraction)
metadata: dict[str, Any] = {}
metadata_raw = payload.get("metadata")
if isinstance(metadata_raw, dict):
metadata = {str(k): v for k, v in metadata_raw.items()}

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
import re
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator
class Severity(str, Enum):
@@ -497,6 +498,214 @@ class TaxCompliancePenalties(BaseModel):
)
_OFFICER_HOURS_PATTERN = re.compile(r"([\d.]+)\s*hrs?/wk", re.IGNORECASE)
def _parse_officer_list(entries: list[str] | None) -> list[dict[str, Any]]:
if not entries:
return []
parsed: list[dict[str, Any]] = []
for raw in entries:
if not isinstance(raw, str):
continue
parts = [part.strip() for part in raw.split(",")]
name = parts[0] if parts else ""
title = parts[1] if len(parts) > 1 else ""
role = parts[3] if len(parts) > 3 else ""
hours = 0.0
match = _OFFICER_HOURS_PATTERN.search(raw)
if match:
try:
hours = float(match.group(1))
except ValueError:
hours = 0.0
parsed.append(
{
"name": name,
"title_position": title,
"average_hours_per_week": hours,
"related_party_transactions": "",
"former_officer": "",
"governance_role": role,
}
)
return parsed
def _build_program_accomplishments(
descriptions: list[str] | None,
) -> list[dict[str, Any]]:
if not descriptions:
return []
programs: list[dict[str, Any]] = []
for idx, description in enumerate(descriptions, start=1):
if not isinstance(description, str):
continue
programs.append(
{
"program_name": f"Program {idx}",
"program_description": description.strip(),
"expenses": 0.0,
"grants": 0.0,
"revenue_generated": 0.0,
"quantitative_outputs": "",
}
)
return programs
def _transform_flat_payload(data: dict[str, Any]) -> dict[str, Any]:
def get_str(key: str) -> str:
value = data.get(key)
if value is None:
return ""
return str(value)
def get_value(key: str, default: Any = 0) -> Any:
return data.get(key, default)
transformed: dict[str, Any] = {
"core_organization_metadata": {
"ein": get_str("ein"),
"legal_name": get_str("legal_name"),
"phone_number": get_str("phone_number"),
"website_url": get_str("website_url"),
"return_type": get_str("return_type"),
"amended_return": get_str("amended_return"),
"group_exemption_number": get_str("group_exemption_number"),
"subsection_code": get_str("subsection_code"),
"ruling_date": get_str("ruling_date"),
"accounting_method": get_str("accounting_method"),
"organization_type": get_str("organization_type"),
"year_of_formation": get_str("year_of_formation"),
"incorporation_state": get_str("incorporation_state"),
},
"revenue_breakdown": {
"total_revenue": get_value("total_revenue"),
"contributions_gifts_grants": get_value("contributions_gifts_grants"),
"program_service_revenue": get_value("program_service_revenue"),
"membership_dues": get_value("membership_dues"),
"investment_income": get_value("investment_income"),
"gains_losses_sales_assets": get_value("gains_losses_sales_assets"),
"rental_income": get_value("rental_income"),
"related_organizations_revenue": get_value("related_organizations_revenue"),
"gaming_revenue": get_value("gaming_revenue"),
"other_revenue": get_value("other_revenue"),
"government_grants": get_value("government_grants"),
"foreign_contributions": get_value("foreign_contributions"),
},
"expenses_breakdown": {
"total_expenses": get_value("total_expenses"),
"program_services_expenses": get_value("program_services_expenses"),
"management_general_expenses": get_value("management_general_expenses"),
"fundraising_expenses": get_value("fundraising_expenses"),
"grants_us_organizations": get_value("grants_us_organizations"),
"grants_us_individuals": get_value("grants_us_individuals"),
"grants_foreign_organizations": get_value("grants_foreign_organizations"),
"grants_foreign_individuals": get_value("grants_foreign_individuals"),
"compensation_officers": get_value("compensation_officers"),
"compensation_other_staff": get_value("compensation_other_staff"),
"payroll_taxes_benefits": get_value("payroll_taxes_benefits"),
"professional_fees": get_value("professional_fees"),
"office_occupancy_costs": get_value("office_occupancy_costs"),
"information_technology_costs": get_value("information_technology_costs"),
"travel_conference_expenses": get_value("travel_conference_expenses"),
"depreciation_amortization": get_value("depreciation_amortization"),
"insurance": get_value("insurance"),
},
"balance_sheet": data.get("balance_sheet") or {},
"officers_directors_trustees_key_employees": _parse_officer_list(
data.get("officers_list")
),
"governance_management_disclosure": {
"governing_body_size": get_value("governing_body_size"),
"independent_members": get_value("independent_members"),
"financial_statements_reviewed": get_str("financial_statements_reviewed"),
"form_990_provided_to_governing_body": get_str(
"form_990_provided_to_governing_body"
),
"conflict_of_interest_policy": get_str("conflict_of_interest_policy"),
"whistleblower_policy": get_str("whistleblower_policy"),
"document_retention_policy": get_str("document_retention_policy"),
"ceo_compensation_review_process": get_str(
"ceo_compensation_review_process"
),
"public_disclosure_practices": get_str("public_disclosure_practices"),
},
"program_service_accomplishments": _build_program_accomplishments(
data.get("program_accomplishments_list")
),
"fundraising_grantmaking": {
"total_fundraising_event_revenue": get_value(
"total_fundraising_event_revenue"
),
"total_fundraising_event_expenses": get_value(
"total_fundraising_event_expenses"
),
"professional_fundraiser_fees": get_value("professional_fundraiser_fees"),
},
"functional_operational_data": {
"number_of_employees": get_value("number_of_employees"),
"number_of_volunteers": get_value("number_of_volunteers"),
"occupancy_costs": get_value("occupancy_costs"),
"fundraising_method_descriptions": get_str(
"fundraising_method_descriptions"
),
"joint_ventures_disregarded_entities": get_str(
"joint_ventures_disregarded_entities"
),
},
"compensation_details": {
"base_compensation": get_value("base_compensation"),
"bonus": get_value("bonus"),
"incentive": get_value("incentive"),
"other": get_value("other_compensation", get_value("other", 0)),
"non_fixed_compensation": get_str("non_fixed_compensation"),
"first_class_travel": get_str("first_class_travel"),
"housing_allowance": get_str("housing_allowance"),
"expense_account_usage": get_str("expense_account_usage"),
"supplemental_retirement": get_str("supplemental_retirement"),
},
"political_lobbying_activities": {
"lobbying_expenditures_direct": get_value("lobbying_expenditures_direct"),
"lobbying_expenditures_grassroots": get_value(
"lobbying_expenditures_grassroots"
),
"election_501h_status": get_str("election_501h_status"),
"political_campaign_expenditures": get_value(
"political_campaign_expenditures"
),
"related_organizations_affiliates": get_str(
"related_organizations_affiliates"
),
},
"investments_endowment": {
"investment_types": get_str("investment_types"),
"donor_restricted_endowment_values": get_value(
"donor_restricted_endowment_values"
),
"net_appreciation_depreciation": get_value("net_appreciation_depreciation"),
"related_organization_transactions": get_str(
"related_organization_transactions"
),
"loans_to_from_related_parties": get_str("loans_to_from_related_parties"),
},
"tax_compliance_penalties": {
"penalties_excise_taxes_reported": get_str(
"penalties_excise_taxes_reported"
),
"unrelated_business_income_disclosure": get_str(
"unrelated_business_income_disclosure"
),
"foreign_bank_account_reporting": get_str("foreign_bank_account_reporting"),
"schedule_o_narrative_explanations": get_str(
"schedule_o_narrative_explanations"
),
},
}
return transformed
class ExtractedIrsForm990PfDataSchema(BaseModel):
core_organization_metadata: CoreOrganizationMetadata = Field(
...,
@@ -514,7 +723,7 @@ class ExtractedIrsForm990PfDataSchema(BaseModel):
title="Expenses Breakdown",
)
balance_sheet: dict[str, Any] = Field(
...,
default_factory=dict,
description="Assets, liabilities, and net assets at year end.",
title="Balance Sheet Data",
)
@@ -566,6 +775,15 @@ class ExtractedIrsForm990PfDataSchema(BaseModel):
title="Tax Compliance / Penalties",
)
@model_validator(mode="before")
@classmethod
def _ensure_structure(cls, value: Any) -> Any:
if not isinstance(value, dict):
return value
if "core_organization_metadata" in value:
return value
return _transform_flat_payload(value)
class ValidatorState(BaseModel):
extraction: ExtractedIrsForm990PfDataSchema

View File

@@ -1,5 +1,9 @@
from fastapi import APIRouter
from pydantic_ai import Agent
import json
from dataclasses import dataclass
from typing import Annotated, Any
from fastapi import APIRouter, Header
from pydantic_ai import Agent, RunContext
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.azure import AzureProvider
from pydantic_ai.ui.vercel_ai import VercelAIAdapter
@@ -8,6 +12,7 @@ from starlette.responses import Response
from app.agents import form_auditor, web_search
from app.core.config import settings
from app.services.extracted_data_service import get_extracted_data_service
provider = AzureProvider(
azure_endpoint=settings.AZURE_OPENAI_ENDPOINT,
@@ -15,347 +20,26 @@ provider = AzureProvider(
api_key=settings.AZURE_OPENAI_API_KEY,
)
model = OpenAIChatModel(model_name="gpt-4o", provider=provider)
agent = Agent(model=model)
@dataclass
class Deps:
extracted_data: dict[str, Any]
agent = Agent(model=model, deps_type=Deps)
router = APIRouter(prefix="/api/v1/agent", tags=["Agent"])
data = {
"extraction": {
"core_organization_metadata": {
"ein": "84-2674654",
"legal_name": "07 IN HEAVEN MEMORIAL SCHOLARSHIP",
"phone_number": "(262) 215-0300",
"website_url": "",
"return_type": "990-PF",
"amended_return": "No",
"group_exemption_number": "",
"subsection_code": "501(c)(3)",
"ruling_date": "",
"accounting_method": "Cash",
"organization_type": "corporation",
"year_of_formation": "",
"incorporation_state": "WI",
},
"revenue_breakdown": {
"total_revenue": 5227,
"contributions_gifts_grants": 5227,
"program_service_revenue": 0,
"membership_dues": 0,
"investment_income": 0,
"gains_losses_sales_assets": 0,
"rental_income": 0,
"related_organizations_revenue": 0,
"gaming_revenue": 0,
"other_revenue": 0,
"government_grants": 0,
"foreign_contributions": 0,
},
"expenses_breakdown": {
"total_expenses": 2104,
"program_services_expenses": 0,
"management_general_expenses": 0,
"fundraising_expenses": 2104,
"grants_us_organizations": 0,
"grants_us_individuals": 0,
"grants_foreign_organizations": 0,
"grants_foreign_individuals": 0,
"compensation_officers": 0,
"compensation_other_staff": 0,
"payroll_taxes_benefits": 0,
"professional_fees": 0,
"office_occupancy_costs": 0,
"information_technology_costs": 0,
"travel_conference_expenses": 0,
"depreciation_amortization": 0,
"insurance": 0,
},
"balance_sheet": {},
"officers_directors_trustees_key_employees": [
{
"name": "REBECCA TERPSTRA",
"title_position": "PRESIDENT",
"average_hours_per_week": 0.1,
"related_party_transactions": "",
"former_officer": "",
"governance_role": "",
},
{
"name": "ROBERT GUZMAN",
"title_position": "VICE PRESDEINT",
"average_hours_per_week": 0.1,
"related_party_transactions": "",
"former_officer": "",
"governance_role": "",
},
{
"name": "ANDREA VALENTI",
"title_position": "TREASURER",
"average_hours_per_week": 0.1,
"related_party_transactions": "",
"former_officer": "",
"governance_role": "",
},
{
"name": "BETHANY WALSH",
"title_position": "SECRETARY",
"average_hours_per_week": 0.1,
"related_party_transactions": "",
"former_officer": "",
"governance_role": "",
},
],
"governance_management_disclosure": {
"governing_body_size": 4,
"independent_members": 4,
"financial_statements_reviewed": "",
"form_990_provided_to_governing_body": "",
"conflict_of_interest_policy": "",
"whistleblower_policy": "",
"document_retention_policy": "",
"ceo_compensation_review_process": "",
"public_disclosure_practices": "Yes",
},
"program_service_accomplishments": [],
"fundraising_grantmaking": {
"total_fundraising_event_revenue": 0,
"total_fundraising_event_expenses": 2104,
"professional_fundraiser_fees": 0,
},
"functional_operational_data": {
"number_of_employees": 0,
"number_of_volunteers": 0,
"occupancy_costs": 0,
"fundraising_method_descriptions": "",
"joint_ventures_disregarded_entities": "",
},
"compensation_details": {
"base_compensation": 0,
"bonus": 0,
"incentive": 0,
"other": 0,
"non_fixed_compensation": "",
"first_class_travel": "",
"housing_allowance": "",
"expense_account_usage": "",
"supplemental_retirement": "",
},
"political_lobbying_activities": {
"lobbying_expenditures_direct": 0,
"lobbying_expenditures_grassroots": 0,
"election_501h_status": "",
"political_campaign_expenditures": 0,
"related_organizations_affiliates": "",
},
"investments_endowment": {
"investment_types": "",
"donor_restricted_endowment_values": 0,
"net_appreciation_depreciation": 0,
"related_organization_transactions": "",
"loans_to_from_related_parties": "",
},
"tax_compliance_penalties": {
"penalties_excise_taxes_reported": "No",
"unrelated_business_income_disclosure": "No",
"foreign_bank_account_reporting": "No",
"schedule_o_narrative_explanations": "",
},
},
"extraction_metadata": {
"core_organization_metadata": {
"ein": {"value": "84-2674654", "references": ["0-7"]},
"legal_name": {
"value": "07 IN HEAVEN MEMORIAL SCHOLARSHIP",
"references": ["0-6"],
},
"phone_number": {"value": "(262) 215-0300", "references": ["0-a"]},
"website_url": {"value": "", "references": []},
"return_type": {
"value": "990-PF",
"references": ["4ade8ed0-bce7-4bd5-bd8d-190e3e4be95b"],
},
"amended_return": {
"value": "No",
"references": ["4ac9edc4-e9bb-430f-b4c4-a42bf4c04b28"],
},
"group_exemption_number": {"value": "", "references": []},
"subsection_code": {
"value": "501(c)(3)",
"references": ["4ac9edc4-e9bb-430f-b4c4-a42bf4c04b28"],
},
"ruling_date": {"value": "", "references": []},
"accounting_method": {"value": "Cash", "references": ["0-d"]},
"organization_type": {
"value": "corporation",
"references": ["4ac9edc4-e9bb-430f-b4c4-a42bf4c04b28"],
},
"year_of_formation": {"value": "", "references": []},
"incorporation_state": {
"value": "WI",
"references": ["4ac9edc4-e9bb-430f-b4c4-a42bf4c04b28"],
},
},
"revenue_breakdown": {
"total_revenue": {"value": 5227, "references": ["0-1z"]},
"contributions_gifts_grants": {"value": 5227, "references": ["0-m"]},
"program_service_revenue": {"value": 0, "references": []},
"membership_dues": {"value": 0, "references": []},
"investment_income": {"value": 0, "references": []},
"gains_losses_sales_assets": {"value": 0, "references": []},
"rental_income": {"value": 0, "references": []},
"related_organizations_revenue": {"value": 0, "references": []},
"gaming_revenue": {"value": 0, "references": []},
"other_revenue": {"value": 0, "references": []},
"government_grants": {"value": 0, "references": []},
"foreign_contributions": {"value": 0, "references": []},
},
"expenses_breakdown": {
"total_expenses": {"value": 2104, "references": ["0-2S"]},
"program_services_expenses": {"value": 0, "references": []},
"management_general_expenses": {"value": 0, "references": []},
"fundraising_expenses": {"value": 2104, "references": ["13-d"]},
"grants_us_organizations": {"value": 0, "references": []},
"grants_us_individuals": {"value": 0, "references": []},
"grants_foreign_organizations": {"value": 0, "references": []},
"grants_foreign_individuals": {"value": 0, "references": []},
"compensation_officers": {
"value": 0,
"references": ["5-1q", "5-1w", "5-1C", "5-1I"],
},
"compensation_other_staff": {"value": 0, "references": []},
"payroll_taxes_benefits": {"value": 0, "references": []},
"professional_fees": {"value": 0, "references": []},
"office_occupancy_costs": {"value": 0, "references": []},
"information_technology_costs": {"value": 0, "references": []},
"travel_conference_expenses": {"value": 0, "references": []},
"depreciation_amortization": {"value": 0, "references": []},
"insurance": {"value": 0, "references": []},
},
"balance_sheet": {},
"officers_directors_trustees_key_employees": [
{
"name": {"value": "REBECCA TERPSTRA", "references": ["5-1o"]},
"title_position": {"value": "PRESIDENT", "references": ["5-1p"]},
"average_hours_per_week": {"value": 0.1, "references": ["5-1p"]},
"related_party_transactions": {"value": "", "references": []},
"former_officer": {"value": "", "references": []},
"governance_role": {"value": "", "references": []},
},
{
"name": {"value": "ROBERT GUZMAN", "references": ["5-1u"]},
"title_position": {
"value": "VICE PRESDEINT",
"references": ["5-1v"],
},
"average_hours_per_week": {"value": 0.1, "references": ["5-1v"]},
"related_party_transactions": {"value": "", "references": []},
"former_officer": {"value": "", "references": []},
"governance_role": {"value": "", "references": []},
},
{
"name": {"value": "ANDREA VALENTI", "references": ["5-1A"]},
"title_position": {"value": "TREASURER", "references": ["5-1B"]},
"average_hours_per_week": {"value": 0.1, "references": ["5-1B"]},
"related_party_transactions": {"value": "", "references": []},
"former_officer": {"value": "", "references": []},
"governance_role": {"value": "", "references": []},
},
{
"name": {"value": "BETHANY WALSH", "references": ["5-1G"]},
"title_position": {"value": "SECRETARY", "references": ["5-1H"]},
"average_hours_per_week": {"value": 0.1, "references": ["5-1H"]},
"related_party_transactions": {"value": "", "references": []},
"former_officer": {"value": "", "references": []},
"governance_role": {"value": "", "references": []},
},
],
"governance_management_disclosure": {
"governing_body_size": {
"value": 4,
"references": ["5-1o", "5-1u", "5-1A", "5-1G"],
},
"independent_members": {
"value": 4,
"references": ["5-1o", "5-1u", "5-1A", "5-1G"],
},
"financial_statements_reviewed": {"value": "", "references": []},
"form_990_provided_to_governing_body": {"value": "", "references": []},
"conflict_of_interest_policy": {"value": "", "references": []},
"whistleblower_policy": {"value": "", "references": []},
"document_retention_policy": {"value": "", "references": []},
"ceo_compensation_review_process": {"value": "", "references": []},
"public_disclosure_practices": {"value": "Yes", "references": ["4-g"]},
},
"program_service_accomplishments": [],
"fundraising_grantmaking": {
"total_fundraising_event_revenue": {"value": 0, "references": []},
"total_fundraising_event_expenses": {
"value": 2104,
"references": ["13-d"],
},
"professional_fundraiser_fees": {"value": 0, "references": []},
},
"functional_operational_data": {
"number_of_employees": {"value": 0, "references": []},
"number_of_volunteers": {"value": 0, "references": []},
"occupancy_costs": {"value": 0, "references": []},
"fundraising_method_descriptions": {"value": "", "references": []},
"joint_ventures_disregarded_entities": {"value": "", "references": []},
},
"compensation_details": {
"base_compensation": {"value": 0, "references": ["5-1q", "5-1w"]},
"bonus": {"value": 0, "references": []},
"incentive": {"value": 0, "references": []},
"other": {"value": 0, "references": []},
"non_fixed_compensation": {"value": "", "references": []},
"first_class_travel": {"value": "", "references": []},
"housing_allowance": {"value": "", "references": []},
"expense_account_usage": {"value": "", "references": []},
"supplemental_retirement": {"value": "", "references": []},
},
"political_lobbying_activities": {
"lobbying_expenditures_direct": {"value": 0, "references": []},
"lobbying_expenditures_grassroots": {"value": 0, "references": []},
"election_501h_status": {"value": "", "references": []},
"political_campaign_expenditures": {"value": 0, "references": []},
"related_organizations_affiliates": {"value": "", "references": []},
},
"investments_endowment": {
"investment_types": {"value": "", "references": []},
"donor_restricted_endowment_values": {"value": 0, "references": []},
"net_appreciation_depreciation": {"value": 0, "references": []},
"related_organization_transactions": {"value": "", "references": []},
"loans_to_from_related_parties": {"value": "", "references": []},
},
"tax_compliance_penalties": {
"penalties_excise_taxes_reported": {
"value": "No",
"references": ["3-I"],
},
"unrelated_business_income_disclosure": {
"value": "No",
"references": ["3-Y"],
},
"foreign_bank_account_reporting": {
"value": "No",
"references": ["4-H"],
},
"schedule_o_narrative_explanations": {"value": "", "references": []},
},
},
"metadata": {
"filename": "markdown.md",
"org_id": None,
"duration_ms": 16656,
"credit_usage": 27.2,
"job_id": "nnmr8lcxtykk5ll5wodjtrnn6",
"version": "extract-20250930",
},
}
@agent.tool_plain
async def build_audit_report():
@agent.tool
async def build_audit_report(ctx: RunContext[Deps]):
"""Calls the audit subagent to get a full audit report of the organization"""
data = ctx.deps.extracted_data
with open("data/audit_report.json", "w") as f:
json.dump(data, f)
result = await form_auditor.build_audit_report(data)
return result.model_dump()
@@ -370,5 +54,13 @@ async def search_web_information(query: str, max_results: int = 5):
@router.post("/chat")
async def chat(request: Request) -> Response:
return await VercelAIAdapter.dispatch_request(request, agent=agent)
async def chat(request: Request, tema: Annotated[str, Header()]) -> Response:
extracted_data_service = get_extracted_data_service()
data = await extracted_data_service.get_by_tema(tema)
extracted_data = [doc.get_extracted_data() for doc in data]
deps = Deps(extracted_data=extracted_data[0])
return await VercelAIAdapter.dispatch_request(request, agent=agent, deps=deps)

View File

@@ -14,8 +14,14 @@ logger = logging.getLogger(__name__)
class DataroomCreate(BaseModel):
name: str
collection: str = ""
storage: str = ""
@property
def collection(self) -> str:
return self.name.lower().replace(" ", "_")
@property
def storage(self) -> str:
return self.name.lower().replace(" ", "_")
class DataroomInfo(BaseModel):
@@ -110,9 +116,9 @@ async def dataroom_info(dataroom_name: str) -> DataroomInfo:
if collection_info_response:
collection_info = {
"vectors_count": collection_info_response.vectors_count,
"indexed_vectors_count": collection_info_response.indexed_vectors_count,
"points_count": collection_info_response.points_count,
"segments_count": collection_info_response.segments_count,
"indexed_vectors_count": collection_info_response.vectors_count,
"points_count": collection_info_response.vectors_count,
"segments_count": collection_info_response.vectors_count,
"status": collection_info_response.status,
}
vector_count = collection_info_response.vectors_count

File diff suppressed because one or more lines are too long

View File

@@ -1,74 +0,0 @@
{
"schema_id": "schema_103b7090a545",
"schema_name": "Testing",
"description": "Informacion de las facturas de taxes (Prueba)",
"fields": [
{
"name": "employed_id",
"type": "string",
"description": "id number from employed",
"required": false,
"min_value": null,
"max_value": null,
"pattern": null
},
{
"name": "ej_numero",
"type": "integer",
"description": "ejemplo",
"required": false,
"min_value": null,
"max_value": null,
"pattern": null
},
{
"name": "ej_decimal",
"type": "float",
"description": "ejemplo",
"required": false,
"min_value": null,
"max_value": null,
"pattern": null
},
{
"name": "ej_booleano",
"type": "boolean",
"description": "ejemplo",
"required": false,
"min_value": null,
"max_value": null,
"pattern": null
},
{
"name": "ej_list",
"type": "array_string",
"description": "ejemplo",
"required": false,
"min_value": null,
"max_value": null,
"pattern": null
},
{
"name": "ej_listnum",
"type": "array_integer",
"description": "ejemplo",
"required": false,
"min_value": null,
"max_value": null,
"pattern": null
},
{
"name": "fecha",
"type": "date",
"description": "ejemplo",
"required": false,
"min_value": null,
"max_value": null,
"pattern": null
}
],
"created_at": "2025-11-07T17:49:18.193078",
"updated_at": "2025-11-07T22:19:53.434529",
"tema": "ULTA",
"is_global": true
}

View File

@@ -31,6 +31,8 @@ services:
ports:
- 6379:6379
- 8001:8001
environment:
REDIS_ARGS: --appendonly yes
volumes:
- ./redis_data:/data
restart: unless-stopped

View File

@@ -1,16 +1,15 @@
import React from "react";
import React, { useState } from "react";
import {
AlertTriangle,
CheckCircle,
XCircle,
FileText,
Building,
Calendar,
AlertCircle,
TrendingUp,
ChevronDown,
ChevronRight,
Shield,
Info,
} from "lucide-react";
import { cn } from "@/lib/utils";
type Severity = "Pass" | "Warning" | "Error";
@@ -45,42 +44,45 @@ interface AuditReportProps {
data: AuditReportData;
}
const getSeverityIcon = (severity: Severity) => {
const getSeverityIcon = (severity: Severity, size = "w-4 h-4") => {
switch (severity) {
case "Pass":
return <CheckCircle className="w-5 h-5 text-green-600" />;
return <CheckCircle className={`${size} text-green-600`} />;
case "Warning":
return <AlertTriangle className="w-5 h-5 text-yellow-600" />;
return <AlertTriangle className={`${size} text-yellow-600`} />;
case "Error":
return <XCircle className="w-5 h-5 text-red-600" />;
return <XCircle className={`${size} text-red-600`} />;
default:
return <AlertCircle className="w-5 h-5 text-gray-600" />;
return <AlertCircle className={`${size} text-gray-600`} />;
}
};
const getSeverityColor = (severity: Severity) => {
switch (severity) {
case "Pass":
return "text-green-700 bg-green-50 border-green-200";
case "Warning":
return "text-yellow-700 bg-yellow-50 border-yellow-200";
case "Error":
return "text-red-700 bg-red-50 border-red-200";
default:
return "text-gray-700 bg-gray-50 border-gray-200";
}
};
const getSeverityBadge = (severity: Severity) => {
const colors = {
Pass: "bg-green-100 text-green-800 border-green-200",
Warning: "bg-yellow-100 text-yellow-800 border-yellow-200",
Error: "bg-red-100 text-red-800 border-red-200",
};
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.8) return "text-green-600";
if (confidence >= 0.6) return "text-yellow-600";
return "text-red-600";
return (
<span
className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium border ${colors[severity]}`}
>
{getSeverityIcon(severity, "w-3 h-3")}
{severity}
</span>
);
};
export const AuditReport: React.FC<AuditReportProps> = ({ data }) => {
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set(),
);
const [showAllFindings, setShowAllFindings] = useState(false);
const {
organisation_ein,
organisation_name,
organisation_ein,
year,
overall_severity,
findings,
@@ -95,186 +97,227 @@ export const AuditReport: React.FC<AuditReportProps> = ({ data }) => {
Error: findings.filter((f) => f.severity === "Error").length,
};
const toggleSection = (section: string) => {
const newExpanded = new Set(expandedSections);
if (newExpanded.has(section)) {
newExpanded.delete(section);
} else {
newExpanded.add(section);
}
setExpandedSections(newExpanded);
};
const criticalFindings = findings.filter((f) => f.severity === "Error");
return (
<div className="w-full bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
<div>
{/* Header */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 p-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Shield className="w-6 h-6 text-blue-600" />
{/* Compact Header */}
<div className="bg-linear-to-r from-blue-50 to-indigo-50 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Shield className="w-6 h-6 text-blue-600" />
<div>
<h3 className="font-semibold text-gray-900">
{organisation_name}
</h3>
<div className="flex items-center gap-3 text-xs text-gray-600">
<span>EIN: {organisation_ein}</span>
{year && <span>{year}</span>}
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
<Building className="w-5 h-5" />
{organisation_name}
</h2>
<div className="flex items-center gap-4 mt-1 text-sm text-gray-600">
<span className="flex items-center gap-1">
<FileText className="w-4 h-4" />
EIN: {organisation_ein}
</span>
{year && (
<span className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{year}
</span>
)}
</div>
</div>
</div>
{/* Overall Status */}
<div
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-full border",
getSeverityColor(overall_severity),
)}
>
{getSeverityIcon(overall_severity)}
<span className="font-medium">{overall_severity}</span>
</div>
</div>
{getSeverityBadge(overall_severity)}
</div>
</div>
{/* Statistics Bar */}
<div className="bg-gray-50 px-4 py-2 border-b border-gray-200">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-700">Audit Summary</h3>
<div className="flex items-center gap-6 text-sm">
{/* Quick Stats */}
<div className="bg-gray-50 px-4 py-3 border-b">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">
Audit Results
</span>
<div className="flex items-center gap-4 text-sm">
{severityStats.Pass > 0 && (
<div className="flex items-center gap-1">
<CheckCircle className="w-4 h-4 text-green-600" />
<span className="text-green-700 font-medium">
<CheckCircle className="w-3 h-3 text-green-600" />
<span className="font-medium text-green-700">
{severityStats.Pass}
</span>
<span className="text-gray-600">Passed</span>
</div>
)}
{severityStats.Warning > 0 && (
<div className="flex items-center gap-1">
<AlertTriangle className="w-4 h-4 text-yellow-600" />
<span className="text-yellow-700 font-medium">
<AlertTriangle className="w-3 h-3 text-yellow-600" />
<span className="font-medium text-yellow-700">
{severityStats.Warning}
</span>
<span className="text-gray-600">Warnings</span>
</div>
)}
{severityStats.Error > 0 && (
<div className="flex items-center gap-1">
<XCircle className="w-4 h-4 text-red-600" />
<span className="text-red-700 font-medium">
<XCircle className="w-3 h-3 text-red-600" />
<span className="font-medium text-red-700">
{severityStats.Error}
</span>
<span className="text-gray-600">Errors</span>
</div>
)}
</div>
</div>
</div>
<div className="p-4 space-y-4">
{/* Summary */}
{overall_summary && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-start gap-2">
<Info className="w-4 h-4 text-blue-600 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-blue-900 text-sm mb-1">
Summary
</h4>
<p className="text-sm text-blue-800">{overall_summary}</p>
</div>
</div>
</div>
</div>
{/* Overall Summary */}
{overall_summary && (
<div className="p-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900 mb-3">
Overall Assessment
</h3>
<p className="text-gray-700 leading-relaxed">{overall_summary}</p>
</div>
)}
{/* Section Summaries */}
{sections.length > 0 && (
<div className="p-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Section Analysis
</h3>
<div className="grid gap-2 sm:grid-cols-2">
{sections.map((section, index) => (
<div
key={index}
className={cn(
"border rounded-lg p-3",
getSeverityColor(section.severity),
)}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium">{section.section}</h4>
<div className="flex items-center gap-2">
{getSeverityIcon(section.severity)}
<span
className={cn(
"text-xs font-medium",
getConfidenceColor(section.confidence),
)}
>
{Math.round(section.confidence * 100)}%
</span>
</div>
</div>
<p className="text-sm opacity-90">{section.summary}</p>
{/* Critical Issues (if any) */}
{criticalFindings.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<h4 className="font-medium text-red-900 text-sm mb-2 flex items-center gap-2">
<XCircle className="w-4 h-4" />
Critical Issues ({criticalFindings.length})
</h4>
<div className="space-y-2">
{criticalFindings.slice(0, 2).map((finding, index) => (
<div key={index} className="text-sm text-red-800">
<span className="font-medium">{finding.category}:</span>{" "}
{finding.message}
</div>
))}
{criticalFindings.length > 2 && (
<button
onClick={() => setShowAllFindings(!showAllFindings)}
className="text-xs text-red-700 hover:text-red-800 font-medium"
>
{showAllFindings
? "Show less"
: `+${criticalFindings.length - 2} more issues`}
</button>
)}
</div>
</div>
)}
{/* Detailed Findings */}
<div className="p-4">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Detailed Findings ({findings.length})
</h3>
<div className="space-y-3">
{findings.map((finding, index) => (
<div
key={index}
className={cn(
"border rounded-lg p-3",
getSeverityColor(finding.severity),
)}
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
{getSeverityIcon(finding.severity)}
<div>
<span className="font-medium">{finding.category}</span>
<span className="text-sm text-gray-500 ml-2">
#{finding.check_id}
{/* Sections Overview */}
{sections.length > 0 && (
<div>
<button
onClick={() => toggleSection("sections")}
className="flex items-center gap-2 w-full text-left p-2 hover:bg-gray-50 rounded-lg"
>
{expandedSections.has("sections") ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
<span className="font-medium text-sm">
Section Analysis ({sections.length})
</span>
</button>
{expandedSections.has("sections") && (
<div className="mt-2 grid gap-2 sm:grid-cols-2">
{sections.map((section, index) => (
<div key={index} className="border rounded-lg p-3 bg-gray-50">
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm">
{section.section}
</span>
<div className="flex items-center gap-1">
{getSeverityIcon(section.severity)}
<span className="text-xs text-gray-600">
{Math.round(section.confidence * 100)}%
</span>
</div>
</div>
<p className="text-xs text-gray-700">{section.summary}</p>
</div>
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-gray-400" />
<span
className={cn(
"text-sm font-medium",
getConfidenceColor(finding.confidence),
)}
>
))}
</div>
)}
</div>
)}
{/* All Findings */}
<div>
<button
onClick={() => toggleSection("findings")}
className="flex items-center gap-2 w-full text-left p-2 hover:bg-gray-50 rounded-lg"
>
{expandedSections.has("findings") ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
<span className="font-medium text-sm">
All Findings ({findings.length})
</span>
</button>
{expandedSections.has("findings") && (
<div className="mt-2 space-y-2">
{findings.map((finding, index) => (
<div key={index} className="border rounded-lg p-3 bg-gray-50">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
{getSeverityIcon(finding.severity)}
<div>
<span className="font-medium text-sm">
{finding.category}
</span>
<span className="text-xs text-gray-500 ml-1">
#{finding.check_id}
</span>
</div>
</div>
<span className="text-xs text-gray-600">
{Math.round(finding.confidence * 100)}% confidence
</span>
</div>
<p className="text-sm text-gray-700 mb-2">
{finding.message}
</p>
{finding.mitigation && (
<div className="bg-white rounded p-2 border">
<span className="text-xs font-medium text-gray-600">
Recommended:
</span>
<p className="text-xs text-gray-700 mt-1">
{finding.mitigation}
</p>
</div>
)}
</div>
<p className="text-sm mb-2 leading-relaxed">
{finding.message}
</p>
{finding.mitigation && (
<div className="bg-white bg-opacity-50 rounded p-2 border border-current border-opacity-20">
<h5 className="font-medium text-sm mb-1">
Recommended Action:
</h5>
<p className="text-sm">{finding.mitigation}</p>
</div>
)}
</div>
))}
</div>
))}
</div>
)}
</div>
{/* Notes */}
{notes && (
<div className="bg-gray-50 p-3 border-t border-gray-200">
<h3 className="text-sm font-medium text-gray-700 mb-2">
Additional Notes
</h3>
<p className="text-sm text-gray-600 italic">{notes}</p>
<div className="border-t pt-3">
<div className="flex items-start gap-2">
<FileText className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-700 text-sm mb-1">
Notes
</h4>
<p className="text-sm text-gray-600 italic">{notes}</p>
</div>
</div>
</div>
)}
</div>

View File

@@ -1,241 +0,0 @@
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import { Message, MessageContent } from "@/components/ai-elements/message";
import {
PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuTrigger,
PromptInputAttachment,
PromptInputAttachments,
PromptInputBody,
PromptInputButton,
PromptInputHeader,
type PromptInputMessage,
PromptInputSelect,
PromptInputSelectContent,
PromptInputSelectItem,
PromptInputSelectTrigger,
PromptInputSelectValue,
PromptInputSubmit,
PromptInputTextarea,
PromptInputFooter,
PromptInputTools,
} from "@/components/ai-elements/prompt-input";
import { Action, Actions } from "@/components/ai-elements/actions";
import { Fragment, useState } from "react";
import { useChat } from "@ai-sdk/react";
import { Response } from "@/components/ai-elements/response";
import { CopyIcon, GlobeIcon, RefreshCcwIcon } from "lucide-react";
import {
Source,
Sources,
SourcesContent,
SourcesTrigger,
} from "@/components/ai-elements/sources";
import {
Reasoning,
ReasoningContent,
ReasoningTrigger,
} from "@/components/ai-elements/reasoning";
import { Loader } from "@/components/ai-elements/loader";
import { DefaultChatTransport } from "ai";
const models = [
{
name: "GPT 4o",
value: "openai/gpt-4o",
},
{
name: "Deepseek R1",
value: "deepseek/deepseek-r1",
},
];
const ChatBotDemo = () => {
const [input, setInput] = useState("");
const [model, setModel] = useState<string>(models[0].value);
const [webSearch, setWebSearch] = useState(false);
const { messages, sendMessage, status, regenerate } = useChat({
transport: new DefaultChatTransport({
api: "/api/v1/chat",
}),
});
const handleSubmit = (message: PromptInputMessage) => {
const hasText = Boolean(message.text);
const hasAttachments = Boolean(message.files?.length);
if (!(hasText || hasAttachments)) {
return;
}
sendMessage(
{
text: message.text || "Sent with attachments",
files: message.files,
},
{
body: {
model: model,
webSearch: webSearch,
},
},
);
setInput("");
};
return (
<div className="max-w-4xl mx-auto p-6 relative size-full h-screen">
<div className="flex flex-col h-full">
<Conversation className="h-full">
<ConversationContent>
{messages.map((message) => (
<div key={message.id}>
{message.role === "assistant" &&
message.parts.filter((part) => part.type === "source-url")
.length > 0 && (
<Sources>
<SourcesTrigger
count={
message.parts.filter(
(part) => part.type === "source-url",
).length
}
/>
{message.parts
.filter((part) => part.type === "source-url")
.map((part, i) => (
<SourcesContent key={`${message.id}-${i}`}>
<Source
key={`${message.id}-${i}`}
href={part.url}
title={part.url}
/>
</SourcesContent>
))}
</Sources>
)}
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return (
<Fragment key={`${message.id}-${i}`}>
<Message from={message.role}>
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
{message.role === "assistant" &&
i === messages.length - 1 && (
<Actions className="mt-2">
<Action
onClick={() => regenerate()}
label="Retry"
>
<RefreshCcwIcon className="size-3" />
</Action>
<Action
onClick={() =>
navigator.clipboard.writeText(part.text)
}
label="Copy"
>
<CopyIcon className="size-3" />
</Action>
</Actions>
)}
</Fragment>
);
case "reasoning":
return (
<Reasoning
key={`${message.id}-${i}`}
className="w-full"
isStreaming={
status === "streaming" &&
i === message.parts.length - 1 &&
message.id === messages.at(-1)?.id
}
>
<ReasoningTrigger />
<ReasoningContent>{part.text}</ReasoningContent>
</Reasoning>
);
default:
return null;
}
})}
</div>
))}
{status === "submitted" && <Loader />}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<PromptInput
onSubmit={handleSubmit}
className="mt-4"
globalDrop
multiple
>
<PromptInputHeader>
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
</PromptInputHeader>
<PromptInputBody>
<PromptInputTextarea
onChange={(e) => setInput(e.target.value)}
value={input}
/>
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger />
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
<PromptInputButton
variant={webSearch ? "default" : "ghost"}
onClick={() => setWebSearch(!webSearch)}
>
<GlobeIcon size={16} />
<span>Search</span>
</PromptInputButton>
<PromptInputSelect
onValueChange={(value) => {
setModel(value);
}}
value={model}
>
<PromptInputSelectTrigger>
<PromptInputSelectValue />
</PromptInputSelectTrigger>
<PromptInputSelectContent>
{models.map((model) => (
<PromptInputSelectItem
key={model.value}
value={model.value}
>
{model.name}
</PromptInputSelectItem>
))}
</PromptInputSelectContent>
</PromptInputSelect>
</PromptInputTools>
<PromptInputSubmit disabled={!input && !status} status={status} />
</PromptInputFooter>
</PromptInput>
</div>
</div>
);
};
export default ChatBotDemo;

View File

@@ -1,8 +1,3 @@
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import { Message, MessageContent } from "@/components/ai-elements/message";
import {
PromptInput,
@@ -31,8 +26,10 @@ import {
Bot,
AlertCircle,
PaperclipIcon,
User,
} from "lucide-react";
import { AuditReport } from "./AuditReport";
import { WebSearchResults } from "./WebSearchResults";
import { Loader } from "@/components/ai-elements/loader";
import { DefaultChatTransport } from "ai";
@@ -53,6 +50,9 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
} = useChat({
transport: new DefaultChatTransport({
api: "/api/v1/agent/chat",
headers: {
tema: selectedTema || "",
},
}),
onError: (error) => {
setError(`Error en el chat: ${error.message}`);
@@ -103,23 +103,6 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
return (
<div className="flex flex-col h-[638px] max-h-[638px]">
{/* Chat Header */}
<div className="border-b border-gray-200 px-6 py-4 flex-shrink-0">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<MessageCircle className="w-5 h-5 text-blue-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
Chat con {selectedTema}
</h3>
<p className="text-sm text-gray-600">
Haz preguntas sobre los documentos de este dataroom
</p>
</div>
</div>
</div>
{/* Chat Content */}
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="max-w-4xl mx-auto w-full space-y-6 p-6">
@@ -159,30 +142,60 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
case "text":
return (
<Fragment key={`${message.id}-${i}`}>
<Message from={message.role} className="max-w-none">
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
{message.role === "user" ? (
<div className="flex items-start gap-3 justify-end">
<div className="flex-1">
<Message
from={message.role}
className="max-w-none"
>
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
</div>
<div className="p-2 rounded-full flex-shrink-0 mt-1 bg-gray-100">
<User className="w-4 h-4 text-gray-600" />
</div>
</div>
) : (
<div className="flex items-start gap-3">
<div className="p-2 rounded-full flex-shrink-0 mt-1 bg-blue-100">
<Bot className="w-4 h-4 text-blue-600" />
</div>
<div className="flex-1">
<Message
from={message.role}
className="max-w-none"
>
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
</div>
</div>
)}
{message.role === "assistant" &&
i === message.parts.length - 1 && (
<Actions className="mt-2">
<Action
onClick={() => regenerate()}
label="Regenerar"
disabled={status === "streaming"}
>
<RefreshCcwIcon className="size-3" />
</Action>
<Action
onClick={() =>
navigator.clipboard.writeText(part.text)
}
label="Copiar"
>
<CopyIcon className="size-3" />
</Action>
</Actions>
<div className="ml-12">
<Actions className="mt-2">
<Action
onClick={() => regenerate()}
label="Regenerar"
disabled={status === "streaming"}
>
<RefreshCcwIcon className="size-3" />
</Action>
<Action
onClick={() =>
navigator.clipboard.writeText(part.text)
}
label="Copiar"
>
<CopyIcon className="size-3" />
</Action>
</Actions>
</div>
)}
</Fragment>
);
@@ -231,6 +244,51 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
default:
return null;
}
case "tool-search_web_information":
switch (part.state) {
case "input-available":
return (
<div
key={`${message.id}-${i}`}
className="flex items-center gap-2 p-4 bg-green-50 rounded-lg border border-green-200"
>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600"></div>
<span className="text-sm text-green-700">
Searching the web...
</span>
</div>
);
case "output-available":
return (
<div
key={`${message.id}-${i}`}
className="mt-4 w-full"
>
<div className="max-w-full overflow-hidden">
<WebSearchResults data={part.output} />
</div>
</div>
);
case "output-error":
return (
<div
key={`${message.id}-${i}`}
className="p-4 bg-red-50 border border-red-200 rounded-lg"
>
<div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-red-600" />
<span className="text-sm font-medium text-red-800">
Error searching the web
</span>
</div>
<p className="text-sm text-red-600 mt-1">
{part.errorText}
</p>
</div>
);
default:
return null;
}
default:
return null;
}

View File

@@ -281,47 +281,6 @@ export function DashboardTab({ selectedTema }: DashboardTabProps) {
</div>
</div>
)}
{/* Collection Details */}
{dataroomInfo.collection_info && (
<div className="mt-8">
<h4 className="text-md font-semibold text-gray-900 mb-4">
Detalles de la Colección
</h4>
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<p className="text-sm font-medium text-gray-600">
Total Vectores
</p>
<p className="text-lg font-bold text-gray-900">
{dataroomInfo.collection_info.vectors_count}
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-600">
Vectores Indexados
</p>
<p className="text-lg font-bold text-gray-900">
{dataroomInfo.collection_info.indexed_vectors_count}
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-600">Puntos</p>
<p className="text-lg font-bold text-gray-900">
{dataroomInfo.collection_info.points_count}
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-600">Segmentos</p>
<p className="text-lg font-bold text-gray-900">
{dataroomInfo.collection_info.segments_count}
</p>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,7 +1,14 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { useFileStore } from "@/stores/fileStore";
import { api } from "@/services/api";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Expand, Minimize2 } from "lucide-react";
import { FilesTab } from "./FilesTab";
import { DashboardTab } from "./DashboardTab";
import { ChatTab } from "./ChatTab";
@@ -11,15 +18,69 @@ interface DataroomViewProps {
}
export function DataroomView({ onProcessingChange }: DataroomViewProps = {}) {
const { selectedTema, files } = useFileStore();
const { selectedTema } = useFileStore();
const [processing, setProcessing] = useState(false);
const [fullscreenTab, setFullscreenTab] = useState<string | null>(null);
const [currentTab, setCurrentTab] = useState("overview");
const handleProcessingChange = (isProcessing: boolean) => {
setProcessing(isProcessing);
onProcessingChange?.(isProcessing);
};
const openFullscreen = (tabValue: string) => {
setFullscreenTab(tabValue);
};
const closeFullscreen = () => {
setFullscreenTab(null);
};
const renderTabContent = (tabValue: string, isFullscreen = false) => {
const className = isFullscreen ? "h-[calc(100vh-8rem)] flex flex-col" : "";
switch (tabValue) {
case "overview":
return (
<div className={className}>
<DashboardTab selectedTema={selectedTema} />
</div>
);
case "files":
return (
<div className={className}>
<FilesTab
selectedTema={selectedTema}
processing={processing}
onProcessingChange={handleProcessingChange}
/>
</div>
);
case "chat":
return (
<div className={className}>
<ChatTab selectedTema={selectedTema} />
</div>
);
default:
return null;
}
};
const getTabTitle = (tabValue: string) => {
switch (tabValue) {
case "overview":
return "Overview";
case "files":
return "Files";
case "chat":
return "Chat";
default:
return "";
}
};
return (
<div className="flex flex-col h-full bg-white">
<div className="border-b border-gray-200 px-6 py-4">
@@ -39,46 +100,86 @@ export function DataroomView({ onProcessingChange }: DataroomViewProps = {}) {
</div>
</div>
<Tabs defaultValue="files" className="flex flex-col flex-1">
<Tabs
value={currentTab}
onValueChange={setCurrentTab}
className="flex flex-col flex-1"
>
<div className="border-b border-gray-200 px-6 py-2">
<TabsList className="flex h-10 w-full items-center gap-2 bg-transparent p-0 justify-start">
<TabsTrigger
value="overview"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
<TabsList className="flex h-10 w-full items-center gap-2 bg-transparent p-0 justify-between">
<div className="flex items-center gap-2">
<TabsTrigger
value="overview"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Overview
</TabsTrigger>
<TabsTrigger
value="files"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Files
</TabsTrigger>
<TabsTrigger
value="chat"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Chat
</TabsTrigger>
</div>
<Button
variant="outline"
size="sm"
onClick={() => openFullscreen(currentTab)}
className="ml-auto"
>
Overview
</TabsTrigger>
<TabsTrigger
value="files"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Files
</TabsTrigger>
<TabsTrigger
value="chat"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Chat
</TabsTrigger>
<Expand className="h-4 w-4" />
<span className="sr-only">Open fullscreen</span>
</Button>
</TabsList>
</div>
<TabsContent value="overview" className="mt-0 flex-1">
<DashboardTab selectedTema={selectedTema} />
{renderTabContent("overview")}
</TabsContent>
<TabsContent value="files" className="mt-0 flex flex-1 flex-col">
<FilesTab
selectedTema={selectedTema}
processing={processing}
onProcessingChange={handleProcessingChange}
/>
{renderTabContent("files")}
</TabsContent>
<TabsContent value="chat" className="mt-0 flex-1">
<ChatTab selectedTema={selectedTema} />
{renderTabContent("chat")}
</TabsContent>
</Tabs>
<Dialog
open={fullscreenTab !== null}
onOpenChange={(open: boolean) => !open && closeFullscreen()}
>
<DialogContent className="max-w-[100vw] max-h-[100vh] w-[100vw] h-[100vh] m-0 rounded-none [&>button]:hidden">
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<DialogTitle className="text-xl font-semibold">
{selectedTema
? `${getTabTitle(fullscreenTab || "")} - ${selectedTema}`
: getTabTitle(fullscreenTab || "")}
</DialogTitle>
<Button
variant="outline"
size="sm"
onClick={closeFullscreen}
className="h-8 w-8 p-0"
>
<Minimize2 className="h-4 w-4" />
<span className="sr-only">Exit fullscreen</span>
</Button>
</DialogHeader>
<div className="flex-1 overflow-hidden">
{fullscreenTab && renderTabContent(fullscreenTab, true)}
</div>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,206 @@
import React, { useState } from "react";
import {
Globe,
ExternalLink,
Search,
ChevronDown,
ChevronRight,
Info,
Star,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface SearchResult {
title: string;
url: string;
content: string;
score?: number;
}
interface WebSearchData {
query: string;
results: SearchResult[];
summary: string;
total_results: number;
}
interface WebSearchResultsProps {
data: WebSearchData;
}
const getScoreColor = (score?: number) => {
if (!score) return "text-gray-500";
if (score >= 0.8) return "text-green-600";
if (score >= 0.6) return "text-yellow-600";
return "text-gray-500";
};
const getScoreStars = (score?: number) => {
if (!score) return 0;
return Math.round(score * 5);
};
const truncateContent = (content: string, maxLength: number = 200) => {
if (content.length <= maxLength) return content;
return content.slice(0, maxLength) + "...";
};
export const WebSearchResults: React.FC<WebSearchResultsProps> = ({ data }) => {
const [expandedResults, setExpandedResults] = useState<Set<number>>(new Set());
const [showAllResults, setShowAllResults] = useState(false);
const { query, results, summary, total_results } = data;
const toggleResult = (index: number) => {
const newExpanded = new Set(expandedResults);
if (newExpanded.has(index)) {
newExpanded.delete(index);
} else {
newExpanded.add(index);
}
setExpandedResults(newExpanded);
};
const visibleResults = showAllResults ? results : results.slice(0, 3);
return (
<div className="w-full bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-green-50 to-emerald-50 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Globe className="w-6 h-6 text-green-600" />
<div>
<h3 className="font-semibold text-gray-900">Web Search Results</h3>
<div className="flex items-center gap-2 text-xs text-gray-600">
<Search className="w-3 h-3" />
<span>"{query}"</span>
</div>
</div>
</div>
<div className="text-right">
<div className="text-sm font-medium text-gray-900">{results.length}</div>
<div className="text-xs text-gray-600">
of {total_results} results
</div>
</div>
</div>
</div>
<div className="p-4 space-y-4">
{/* Summary */}
{summary && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-start gap-2">
<Info className="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium text-blue-900 text-sm mb-1">
Summary
</h4>
<p className="text-sm text-blue-800">{summary}</p>
</div>
</div>
</div>
)}
{/* Search Results */}
<div className="space-y-3">
{visibleResults.map((result, index) => {
const isExpanded = expandedResults.has(index);
const stars = getScoreStars(result.score);
return (
<div key={index} className="border border-gray-200 rounded-lg overflow-hidden">
<div className="p-3">
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<h4 className="font-medium text-gray-900 text-sm mb-1 line-clamp-2">
{result.title}
</h4>
<div className="flex items-center gap-2">
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1 truncate"
>
<ExternalLink className="w-3 h-3 flex-shrink-0" />
{new URL(result.url).hostname}
</a>
{result.score && (
<div className="flex items-center gap-1">
<div className="flex">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={cn(
"w-3 h-3",
i < stars
? "text-yellow-400 fill-current"
: "text-gray-300"
)}
/>
))}
</div>
<span className={cn("text-xs font-medium", getScoreColor(result.score))}>
{Math.round((result.score || 0) * 100)}%
</span>
</div>
)}
</div>
</div>
</div>
<div className="text-sm text-gray-700">
{isExpanded ? result.content : truncateContent(result.content)}
</div>
{result.content.length > 200 && (
<button
onClick={() => toggleResult(index)}
className="mt-2 flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800 font-medium"
>
{isExpanded ? (
<>
<ChevronDown className="w-3 h-3" />
Show less
</>
) : (
<>
<ChevronRight className="w-3 h-3" />
Read more
</>
)}
</button>
)}
</div>
</div>
);
})}
</div>
{/* Show More/Less Button */}
{results.length > 3 && (
<div className="text-center">
<button
onClick={() => setShowAllResults(!showAllResults)}
className="text-sm text-blue-600 hover:text-blue-800 font-medium px-4 py-2 rounded-lg hover:bg-blue-50 transition-colors"
>
{showAllResults
? "Show fewer results"
: `Show ${results.length - 3} more results`}
</button>
</div>
)}
{/* No Results */}
{results.length === 0 && (
<div className="text-center py-6">
<Search className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600">No results found for "{query}"</p>
</div>
)}
</div>
</div>
);
};

Binary file not shown.

View File

@@ -0,0 +1,302 @@
*2
$6
SELECT
$1
0
*24
$14
FT._CREATEIFNX
$23
extracted_doc:doc:index
$2
ON
$4
HASH
$6
PREFIX
$1
1
$18
extracted_doc:doc:
$6
SCHEMA
$2
pk
$3
TAG
$9
SEPARATOR
$1
|
$9
file_name
$3
TAG
$9
SEPARATOR
$1
|
$4
tema
$3
TAG
$9
SEPARATOR
$1
|
$15
collection_name
$3
TAG
$9
SEPARATOR
$1
|
*3
$3
SET
$28
extracted_doc:doc:index:hash
$40
9de4cb60a645142de3d0f914909eb21259ea256c
*12
$14
FT._CREATEIFNX
$35
:app.models.dataroom.DataRoom:index
$2
ON
$4
HASH
$6
PREFIX
$1
1
$30
:app.models.dataroom.DataRoom:
$6
SCHEMA
$2
pk
$3
TAG
$9
SEPARATOR
$1
|
*3
$3
SET
$40
:app.models.dataroom.DataRoom:index:hash
$40
5ba27839f9c1b369df2b0734904dd56c02d2cea5
*10
$4
HSET
$56
:app.models.dataroom.DataRoom:01K9J296J6FHT5AKG92D2VX9VW
$2
pk
$26
01K9J296J6FHT5AKG92D2VX9VW
$4
name
$10
ABBEY C.U.
$10
collection
$0
$7
storage
$0
*2
$3
DEL
$56
:app.models.dataroom.DataRoom:01K9J296J6FHT5AKG92D2VX9VW
*10
$4
HSET
$56
:app.models.dataroom.DataRoom:01K9J2J0A0HPWJQEY0PXFCVPW8
$2
pk
$26
01K9J2J0A0HPWJQEY0PXFCVPW8
$4
name
$4
ABBY
$10
collection
$0
$7
storage
$0
*2
$3
DEL
$56
:app.models.dataroom.DataRoom:01K9J2J0A0HPWJQEY0PXFCVPW8
*10
$4
HSET
$56
:app.models.dataroom.DataRoom:01K9J2R5ZGS96G60P0G80W248Z
$2
pk
$26
01K9J2R5ZGS96G60P0G80W248Z
$4
name
$4
ABBY
$10
collection
$4
abby
$7
storage
$4
abby
*2
$6
SELECT
$1
0
*14
$4
HSET
$44
extracted_doc:doc:01K9J44CG40W28WKNC6RD5QSGM
$2
pk
$26
01K9J44CG40W28WKNC6RD5QSGM
$9
file_name
$21
tax_year_2022_990.pdf
$4
tema
$4
ABBY
$15
collection_name
$4
ABBY
$19
extracted_data_json
$5684
{
"ein": "31-0329725",
"legal_name": "0220 ABBEY CREDIT UNION INC",
"phone_number": "(397) 898-7800",
"website_url": "www.abbeycu.com",
"return_type": "990",
"amended_return": "",
"group_exemption_number": "",
"subsection_code": "501(c)(14)",
"ruling_date": "",
"accounting_method": "Accrual",
"organization_type": "Corporation",
"year_of_formation": "1937",
"incorporation_state": "OH",
"total_revenue": 6146738,
"contributions_gifts_grants": 0,
"program_service_revenue": 5656278,
"membership_dues": 0,
"investment_income": 490460,
"gains_losses_sales_assets": 0,
"rental_income": 0,
"related_organizations_revenue": 0,
"gaming_revenue": 0,
"other_revenue": 0,
"government_grants": 0,
"foreign_contributions": 0,
"total_expenses": 5526970,
"program_services_expenses": 386454,
"management_general_expenses": 5140516,
"fundraising_expenses": 0,
"grants_us_organizations": 0,
"grants_us_individuals": 0,
"grants_foreign_organizations": 0,
"grants_foreign_individuals": 0,
"compensation_officers": 629393,
"compensation_other_staff": 1329887,
"payroll_taxes_benefits": 452511,
"professional_fees": 96471,
"office_occupancy_costs": 292244,
"information_technology_costs": 287125,
"travel_conference_expenses": 26662,
"depreciation_amortization": 236836,
"insurance": 39570,
"officers_list": [
"Lisa Burk, Chief Experience Officer, 40.00 hrs/wk, Officer, $73,286 compensation, $8,931 other compensation",
"Eric Stetzel, VP of Business Services, 40.00 hrs/wk, Key employee, $111,064 compensation, $15,979 other compensation",
"Dean Pielemeier, CEO, 40.00 hrs/wk, Officer, $212,994 compensation, $20,202 other compensation",
"Blanca Criner, Chief Marketing and Business Development Officer, 40.00 hrs/wk, Officer, $121,036 compensation, $4,540 other compensation",
"Teri Puthoff, CFO, 40.00 hrs/wk, Officer, $120,962 compensation, $15,754 other compensation",
"Michael Thein, Chairman, 1.00 hrs/wk, Individual trustee or director, $0 compensation",
"Latham Farley, Board Member, 1.00 hrs/wk, Individual trustee or director, $0 compensation",
"Steve Wilmoth, Treasurer, 1.00 hrs/wk, Individual trustee or director, $0 compensation",
"Julie Trick, Secretary, 1.00 hrs/wk, Individual trustee or director, $0 compensation",
"Michele Blake, Board Member, 1.00 hrs/wk, Individual trustee or director, $0 compensation",
"Cheryl Saunders, Board Member, 1.00 hrs/wk, Individual trustee or director, $0 compensation",
"Heather Scaggs-Richardson, Vice Chairman, 1.00 hrs/wk, Individual trustee or director, $0 compensation"
],
"governing_body_size": 7,
"independent_members": 7,
"financial_statements_reviewed": "No",
"form_990_provided_to_governing_body": "Yes",
"conflict_of_interest_policy": "Yes",
"whistleblower_policy": "Yes",
"document_retention_policy": "Yes",
"ceo_compensation_review_process": "Compensation committee, independent compensation consultant, approval by the board or compensation committee",
"public_disclosure_practices": "Audited financial statements are handed out each year at the annual meeting. Other documents are not made available to the public unless requested.",
"program_accomplishments_list": [
"Our mission is to help our members improve their economic well being and quality of life by being competitive convenient and cutting edge.",
"Abbey provides checking savings accounts money market accounts certificates of deposit and IRAs. Abbey also offers free atm debit card services wire transfer services mobile banking and insurance related products.",
"Abbey provides personal vehicle mortgage and credit card loans to its members."
],
"total_fundraising_event_revenue": 0,
"total_fundraising_event_expenses": 0,
"professional_fundraiser_fees": 0,
"number_of_employees": 50,
"number_of_volunteers": 7,
"occupancy_costs": 132399,
"fundraising_method_descriptions": "",
"joint_ventures_disregarded_entities": "",
"base_compensation": 546779,
"bonus": 34415,
"incentive": 59692,
"other_compensation": 24691,
"non_fixed_compensation": "",
"first_class_travel": "No",
"housing_allowance": "No",
"expense_account_usage": "No",
"supplemental_retirement": "No",
"lobbying_expenditures_direct": 0,
"lobbying_expenditures_grassroots": 0,
"election_501h_status": "",
"political_campaign_expenditures": 0,
"related_organizations_affiliates": "",
"investment_types": "Publicly traded securities",
"donor_restricted_endowment_values": 0,
"net_appreciation_depreciation": -2190939,
"related_organization_transactions": "",
"loans_to_from_related_parties": "Lynn Cook (Retired CEO) Life Insurance loan, $1,400,000; Dean Pielemeier (CEO) Life Insurance loan, $1,239,997",
"penalties_excise_taxes_reported": "No",
"unrelated_business_income_disclosure": "Yes",
"foreign_bank_account_reporting": "No",
"schedule_o_narrative_explanations": "Membership to Abbey is open to those who live work worship or attend school in Montgomery Miami Shelby Darke or Greene counties. Abbey CU is owned by the people who open a savings account at Abbey CU. Each member has one vote at the annual election of the Board of Directors. Decisions that require approval: Ohio Division of Financial Institutions Pursuant to regulation. CFO prepares the form and CEO reviews before submitting. Approval of Compensation Committee and Board of Directors for CEO compensation. CEO establishes compensation for Key Employees. Audited financial statements are handed out each year at the annual meeting. Other documents are not made available to the public unless requested."
}
$20
extraction_timestamp
$26
2025-11-08T16:17:31.137820

Binary file not shown.