add agent context
This commit is contained in:
@@ -11,15 +11,23 @@ from .models import (
|
||||
|
||||
|
||||
async def build_audit_report(payload: dict[str, Any]) -> AuditReport:
|
||||
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()}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
1
backend/data/audit_report.json
Normal file
1
backend/data/audit_report.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
}
|
||||
@@ -31,6 +31,8 @@ services:
|
||||
ports:
|
||||
- 6379:6379
|
||||
- 8001:8001
|
||||
environment:
|
||||
REDIS_ARGS: --appendonly yes
|
||||
volumes:
|
||||
- ./redis_data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -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,187 +97,228 @@ 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" />
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* Statistics Bar */}
|
||||
<div className="bg-gray-50 px-4 py-2 border-b border-gray-200">
|
||||
{/* Compact Header */}
|
||||
<div className="bg-linear-to-r from-blue-50 to-indigo-50 p-4">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
{getSeverityBadge(overall_severity)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Overall Summary */}
|
||||
<div className="p-4 space-y-4">
|
||||
{/* 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 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>
|
||||
)}
|
||||
|
||||
{/* Section Summaries */}
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Sections Overview */}
|
||||
{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">
|
||||
<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={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">
|
||||
<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={cn(
|
||||
"text-xs font-medium",
|
||||
getConfidenceColor(section.confidence),
|
||||
)}
|
||||
>
|
||||
<span className="text-xs text-gray-600">
|
||||
{Math.round(section.confidence * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm opacity-90">{section.summary}</p>
|
||||
<p className="text-xs text-gray-700">{section.summary}</p>
|
||||
</div>
|
||||
))}
|
||||
</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),
|
||||
)}
|
||||
{/* 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"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
{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">{finding.category}</span>
|
||||
<span className="text-sm text-gray-500 ml-2">
|
||||
<span className="font-medium text-sm">
|
||||
{finding.category}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 ml-1">
|
||||
#{finding.check_id}
|
||||
</span>
|
||||
</div>
|
||||
</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),
|
||||
)}
|
||||
>
|
||||
<span className="text-xs text-gray-600">
|
||||
{Math.round(finding.confidence * 100)}% confidence
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm mb-2 leading-relaxed">
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
{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 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>
|
||||
))}
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
@@ -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,13 +142,42 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
|
||||
case "text":
|
||||
return (
|
||||
<Fragment key={`${message.id}-${i}`}>
|
||||
<Message from={message.role} className="max-w-none">
|
||||
{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 && (
|
||||
<div className="ml-12">
|
||||
<Actions className="mt-2">
|
||||
<Action
|
||||
onClick={() => regenerate()}
|
||||
@@ -183,6 +195,7 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
|
||||
<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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,9 +100,14 @@ 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">
|
||||
<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"
|
||||
@@ -60,25 +126,60 @@ export function DataroomView({ onProcessingChange }: DataroomViewProps = {}) {
|
||||
>
|
||||
Chat
|
||||
</TabsTrigger>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => openFullscreen(currentTab)}
|
||||
className="ml-auto"
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
206
frontend/src/components/WebSearchResults.tsx
Normal file
206
frontend/src/components/WebSearchResults.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
BIN
redis_data/appendonlydir/appendonly.aof.1.base.rdb
Normal file
BIN
redis_data/appendonlydir/appendonly.aof.1.base.rdb
Normal file
Binary file not shown.
302
redis_data/appendonlydir/appendonly.aof.1.incr.aof
Normal file
302
redis_data/appendonlydir/appendonly.aof.1.incr.aof
Normal 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.
Reference in New Issue
Block a user