diff --git a/applications/Unity.AutoUI/cypress/scripts/README.md b/applications/Unity.AutoUI/cypress/scripts/README.md index 528f451a64..ce63c13c27 100644 --- a/applications/Unity.AutoUI/cypress/scripts/README.md +++ b/applications/Unity.AutoUI/cypress/scripts/README.md @@ -42,7 +42,7 @@ Add or update the token in `cypress.env.json`: ```json { - "CHEFS_AUTH_TOKEN": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." + "CHEFS_AUTH_TOKEN": "Bearer *..." } ``` diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptDto.cs new file mode 100644 index 0000000000..0bdff63a08 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Application.Dtos; + +namespace Unity.AI.Prompts; + +public class AIPromptDto : AuditedEntityDto +{ + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public PromptType Type { get; set; } + public bool IsActive { get; set; } + public List Versions { get; set; } = new(); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptVersionDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptVersionDto.cs new file mode 100644 index 0000000000..a9ed4e50a8 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/AIPromptVersionDto.cs @@ -0,0 +1,20 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace Unity.AI.Prompts; + +public class AIPromptVersionDto : AuditedEntityDto +{ + public Guid PromptId { get; set; } + public int VersionNumber { get; set; } + public string SystemPrompt { get; set; } = string.Empty; + public string UserPromptTemplate { get; set; } = string.Empty; + public string? DeveloperNotes { get; set; } + public string? TargetModel { get; set; } + public string? TargetProvider { get; set; } + public double Temperature { get; set; } + public int? MaxTokens { get; set; } + public bool IsPublished { get; set; } + public bool IsDeprecated { get; set; } + public string? MetadataJson { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptDto.cs new file mode 100644 index 0000000000..6d361fd3ba --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptDto.cs @@ -0,0 +1,22 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Unity.AI.Prompts; + +public class CreateUpdateAIPromptDto +{ + [Required] + [MaxLength(200)] + [DisplayName("PromptName")] + public string Name { get; set; } = string.Empty; + + [MaxLength(2000)] + [DisplayName("PromptDescription")] + public string? Description { get; set; } + + [DisplayName("PromptType")] + public PromptType Type { get; set; } + + [DisplayName("PromptIsActive")] + public bool IsActive { get; set; } = true; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptVersionDto.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptVersionDto.cs new file mode 100644 index 0000000000..8e3414943f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/CreateUpdateAIPromptVersionDto.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Unity.AI.Prompts; + +public class CreateUpdateAIPromptVersionDto +{ + public Guid PromptId { get; set; } + + [DisplayName("VersionNumber")] + public int VersionNumber { get; set; } + + [Required] + [DisplayName("SystemPrompt")] + public string SystemPrompt { get; set; } = string.Empty; + + [Required] + [DisplayName("UserPromptTemplate")] + public string UserPromptTemplate { get; set; } = string.Empty; + + [DisplayName("DeveloperNotes")] + public string? DeveloperNotes { get; set; } + + [MaxLength(100)] + [DisplayName("TargetModel")] + public string? TargetModel { get; set; } + + [MaxLength(100)] + [DisplayName("TargetProvider")] + public string? TargetProvider { get; set; } + + [DisplayName("Temperature")] + public double Temperature { get; set; } = 0.2; + + [DisplayName("MaxTokens")] + public int? MaxTokens { get; set; } + + [DisplayName("IsPublished")] + public bool IsPublished { get; set; } + + [DisplayName("IsDeprecated")] + public bool IsDeprecated { get; set; } + + [DisplayName("MetadataJson")] + public string? MetadataJson { get; set; } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptAppService.cs new file mode 100644 index 0000000000..c259996db0 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptAppService.cs @@ -0,0 +1,13 @@ +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Unity.AI.Prompts; + +public interface IAIPromptAppService : ICrudAppService< + AIPromptDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateAIPromptDto> +{ +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptVersionAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptVersionAppService.cs new file mode 100644 index 0000000000..269029e5ec --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application.Contracts/Prompts/IAIPromptVersionAppService.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Unity.AI.Prompts; + +public interface IAIPromptVersionAppService : ICrudAppService< + AIPromptVersionDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateAIPromptVersionDto> +{ + System.Threading.Tasks.Task> GetByPromptAsync(Guid promptId); +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationAutoMapperProfile.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationAutoMapperProfile.cs index 874ac789db..382fcee837 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationAutoMapperProfile.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationAutoMapperProfile.cs @@ -1,4 +1,6 @@ using AutoMapper; +using Unity.AI.Domain; +using Unity.AI.Prompts; namespace Unity.AI; @@ -6,6 +8,10 @@ public class AIApplicationAutoMapperProfile : Profile { public AIApplicationAutoMapperProfile() { - // Define AutoMapper mappings here as entities and DTOs are introduced + CreateMap(); + CreateMap(MemberList.None); + + CreateMap(); + CreateMap(MemberList.None); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs index 60974f22b4..6c9b4cc5b6 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/AIApplicationModule.cs @@ -52,5 +52,7 @@ public override void ConfigureServices(ServiceConfigurationContext context) { options.ConventionalControllers.Create(typeof(AIApplicationModule).Assembly); }); + + context.Services.AddAssemblyOf(); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs new file mode 100644 index 0000000000..dc0edc9a78 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/DataSeed/AIPromptDataSeeder.cs @@ -0,0 +1,607 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Unity.AI.Domain; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; + +namespace Unity.AI.DataSeed; + +/// +/// Seeds the built-in AI prompts (analysis, attachment, scoresheet) into the host database. +/// Each prompt is seeded with two versions — v0 (original single-file prompts) and v1 (modular +/// prompts with separate rubric, score, output, and rules sections stored in MetadataJson). +/// The seeder is idempotent: it checks by fixed GUID before inserting. +/// +public class AIPromptDataSeeder( + IRepository promptRepository, + IRepository versionRepository, + ICurrentTenant currentTenant) : IDataSeedContributor, ITransientDependency +{ + // Fixed deterministic GUIDs — never change these; they ensure idempotent re-seeding + private static readonly Guid AnalysisPromptId = new("4a100001-1000-4000-a000-000000000001"); + private static readonly Guid AttachmentPromptId = new("4a100001-1000-4000-a000-000000000002"); + private static readonly Guid ScoresheetPromptId = new("4a100001-1000-4000-a000-000000000003"); + + public async Task SeedAsync(DataSeedContext context) + { + if (context.TenantId != null) return; // host database only + + using (currentTenant.Change(null)) + { + await SeedAnalysisPromptAsync(); + await SeedAttachmentPromptAsync(); + await SeedScoresheetPromptAsync(); + } + } + + // ─── ANALYSIS ──────────────────────────────────────────────────────────── + + private async Task SeedAnalysisPromptAsync() + { + if (await promptRepository.AnyAsync(p => p.Id == AnalysisPromptId)) return; + + await promptRepository.InsertAsync(new AIPrompt(AnalysisPromptId, "analysis", PromptType.Skill) + { + Description = "Grant application analysis and review", + IsActive = true + }); + + await versionRepository.InsertAsync(new AIPromptVersion( + Guid.CreateVersion7(), AnalysisPromptId, 0, + AnalysisSystemV0, AnalysisUserV0) + { + DeveloperNotes = "v0 — initial single-file analysis prompt", + IsPublished = true + }); + + await versionRepository.InsertAsync(new AIPromptVersion( + Guid.CreateVersion7(), AnalysisPromptId, 1, + AnalysisSystemV1, AnalysisUserV1) + { + DeveloperNotes = "v1 — modular prompt with separate rubric, score, output, and rules sections", + IsPublished = true, + MetadataJson = BuildSections( + rubric: AnalysisRubric, + score: AnalysisScore, + output: AnalysisOutput, + rules: AnalysisRules, + commonRules: CommonRules) + }); + } + + // ─── ATTACHMENT ─────────────────────────────────────────────────────────── + + private async Task SeedAttachmentPromptAsync() + { + if (await promptRepository.AnyAsync(p => p.Id == AttachmentPromptId)) return; + + await promptRepository.InsertAsync(new AIPrompt(AttachmentPromptId, "attachment", PromptType.Skill) + { + Description = "Attachment summarization for grant review", + IsActive = true + }); + + await versionRepository.InsertAsync(new AIPromptVersion( + Guid.CreateVersion7(), AttachmentPromptId, 0, + AttachmentSystemV0, AttachmentUserV0) + { + DeveloperNotes = "v0 — initial single-file attachment prompt", + IsPublished = true + }); + + await versionRepository.InsertAsync(new AIPromptVersion( + Guid.CreateVersion7(), AttachmentPromptId, 1, + AttachmentSystemV1, AttachmentUserV1) + { + DeveloperNotes = "v1 — modular prompt with separate output and rules sections", + IsPublished = true, + MetadataJson = BuildSections( + output: AttachmentOutput, + rules: AttachmentRules, + commonRules: CommonRules) + }); + } + + // ─── SCORESHEET ─────────────────────────────────────────────────────────── + + private async Task SeedScoresheetPromptAsync() + { + if (await promptRepository.AnyAsync(p => p.Id == ScoresheetPromptId)) return; + + await promptRepository.InsertAsync(new AIPrompt(ScoresheetPromptId, "scoresheet", PromptType.Skill) + { + Description = "Scoresheet section answering assistant", + IsActive = true + }); + + await versionRepository.InsertAsync(new AIPromptVersion( + Guid.CreateVersion7(), ScoresheetPromptId, 0, + ScoresheetSystemV0, ScoresheetUserV0) + { + DeveloperNotes = "v0 — initial single-file scoresheet prompt", + IsPublished = true + }); + + await versionRepository.InsertAsync(new AIPromptVersion( + Guid.CreateVersion7(), ScoresheetPromptId, 1, + ScoresheetSystemV1, ScoresheetUserV1) + { + DeveloperNotes = "v1 — modular prompt with separate output and rules sections", + IsPublished = true, + MetadataJson = BuildSections( + output: ScoresheetOutput, + rules: ScoresheetRules, + commonRules: CommonRules) + }); + } + + // ─── HELPERS ────────────────────────────────────────────────────────────── + + private static string BuildSections( + string? rubric = null, string? score = null, + string? output = null, string? rules = null, string? commonRules = null) + { + var dict = new Dictionary(); + if (rubric != null) dict["RUBRIC"] = rubric; + if (score != null) dict["SCORE"] = score; + if (output != null) dict["OUTPUT"] = output; + if (rules != null) dict["RULES"] = rules; + if (commonRules != null) dict["COMMON_RULES"] = commonRules; + return JsonSerializer.Serialize(new { sections = dict }); + } + + // ═════════════════════════════════════════════════════════════════════════ + // PROMPT CONTENT — mirrors AI/Prompts/Versions/ text files verbatim + // ═════════════════════════════════════════════════════════════════════════ + + // ── v0/analysis.system.txt ─────────────────────────────────────────────── + private const string AnalysisSystemV0 = """ + You are an expert grant application reviewer for the BC Government. + + Conduct a thorough, comprehensive analysis across all rubric areas. Identify substantive issues, concerns, and opportunities for improvement. + + Classify findings by their effect on the application's quality and fundability: + - ERRORS: important missing information, significant gaps, compliance issues, or major concerns affecting eligibility + - WARNINGS: areas needing clarification, moderate issues, or concerns that should be addressed + - SUMMARIES: concise reviewer-facing recommendations or follow-up considerations + + Evaluate content quality, clarity, and appropriateness. Be thorough but fair and avoid nitpicking. + + Respond only with valid JSON in the exact format requested. + """; + + // ── v0/analysis.user.txt ───────────────────────────────────────────────── + private const string AnalysisUserV0 = """ + APPLICATION CONTENT: + {{DATA}} + + ATTACHMENT SUMMARIES: + {{ATTACHMENTS}} + + FORM FIELD CONFIGURATION: + {{SCHEMA}} + + MANDATORY FIELDS: + - Determine mandatory fields from FORM FIELD CONFIGURATION. + - Report missing mandatory fields as findings when they materially affect review quality. + + OPTIONAL FIELDS (may be left blank): + - Determine optional fields from FORM FIELD CONFIGURATION. + - Do not flag optional fields when blank unless they materially weaken rubric evidence. + + EVALUATION RUBRIC: + BC GOVERNMENT GRANT EVALUATION RUBRIC: + + 1. ELIGIBILITY REQUIREMENTS: + - Project must align with program objectives + - Applicant must be eligible entity type + - Budget must be reasonable and well-justified + - Project timeline must be realistic + + 2. COMPLETENESS CHECKS: + - All required fields completed + - Necessary supporting documents provided + - Budget breakdown detailed and accurate + - Project description clear and comprehensive + + 3. FINANCIAL REVIEW: + - Requested amount is within program limits + - Budget is reasonable for scope of work + - Matching funds or in-kind contributions identified + - Cost per outcome/beneficiary is reasonable + + 4. RISK ASSESSMENT: + - Applicant capacity to deliver project + - Technical feasibility of proposed work + - Environmental or regulatory compliance + - Potential for cost overruns or delays + + 5. QUALITY INDICATORS: + - Clear project objectives and outcomes + - Well-defined target audience/beneficiaries + - Appropriate project methodology + - Sustainability plan for long-term impact + + EVALUATION CRITERIA: + - HIGH: Meets all requirements, well-prepared application, low risk + - MEDIUM: Meets most requirements, minor issues or missing elements + - LOW: Missing key requirements, significant concerns, high risk + + Analyze this grant application comprehensively across all five rubric categories (Eligibility, Completeness, Financial Review, Risk Assessment, and Quality Indicators). Identify issues, concerns, and areas for improvement. + + OUTPUT + { + "rating": "", + "warnings": [ + { + "title": "", + "detail": "" + } + ], + "errors": [ + { + "title": "", + "detail": "" + } + ], + "summaries": [ + { + "title": "", + "detail": "" + } + ], + "nextSteps": [ + { + "title": "", + "detail": "" + } + ], + "recommendation": { + "decision": "", + "rationale": "" + } + } + + Important: + - Use only APPLICATION CONTENT, ATTACHMENT SUMMARIES, FORM FIELD CONFIGURATION, and EVALUATION RUBRIC as evidence. + - Use summaries for overall application quality/readiness synthesis. + - Use nextSteps for reviewer-facing follow-up actions or considerations before scoring or decision-making. + - recommendation.decision must be PROCEED or HOLD. + - recommendation.rationale must explain the high-level recommendation in 1-2 complete sentences using provided evidence. + - Use "title" and "detail" keys for all finding objects. + - Return valid plain JSON only in the exact OUTPUT shape. + """; + + // ── v1/analysis.system.txt ─────────────────────────────────────────────── + private const string AnalysisSystemV1 = """ + ROLE + You are a careful grant review assistant for human reviewers. Do not fill gaps, assume compliance, or treat relevance as proof. + + TASK + Using SCHEMA, DATA, ATTACHMENTS, RUBRIC, SCORE, OUTPUT, and RULES: + 1. Review the application and attachments for the strongest reviewer-relevant evidence. + 2. Determine which conclusions are directly supported by that evidence. + 3. Exclude weak, repetitive, or loosely supported conclusions. + 4. Return only the strongest evidence-backed reviewer conclusions. + """; + + // ── v1/analysis.user.txt ───────────────────────────────────────────────── + private const string AnalysisUserV1 = """ + SCHEMA + {{SCHEMA}} + + DATA + {{DATA}} + + ATTACHMENTS + {{ATTACHMENTS}} + + RUBRIC + {{RUBRIC}} + + SCORE + {{SCORE}} + + OUTPUT + {{OUTPUT}} + + RULES + {{RULES}} + {{COMMON_RULES}} + """; + + // ── v1/analysis.rubric.txt ─────────────────────────────────────────────── + private const string AnalysisRubric = """ + ELIGIBILITY REQUIREMENTS: Project aligns with program objectives; Applicant is an eligible entity; Budget is reasonable and justified; Timeline is realistic. + COMPLETENESS CHECKS: Required information is present; Supporting materials are provided where applicable; Description is clear. + FINANCIAL REVIEW: Requested amount is within limits; Budget matches scope; Matching funds or contributions are identified. + RISK ASSESSMENT: Applicant capacity; Feasibility; Compliance considerations; Delivery risks. + QUALITY INDICATORS: Clear objectives; Defined beneficiaries; Appropriate approach; Long-term sustainability. + """; + + // ── v1/analysis.score.txt ──────────────────────────────────────────────── + private const string AnalysisScore = """ + HIGH: Application demonstrates strong evidence across most rubric areas with few or no issues. + MEDIUM: Application has some gaps or weaknesses that require reviewer attention. + LOW: Application has significant gaps or risks across key rubric areas. + """; + + // ── v1/analysis.output.txt ─────────────────────────────────────────────── + private const string AnalysisOutput = """ + { + "rating": "", + "errors": [ + { + "title": "", + "detail": "" + } + ], + "warnings": [ + { + "title": "", + "detail": "" + } + ], + "summaries": [ + { + "title": "", + "detail": "" + } + ], + "nextSteps": [ + { + "title": "", + "detail": "" + } + ], + "recommendation": { + "decision": "", + "rationale": "" + } + } + """; + + // ── v1/analysis.rules.txt ──────────────────────────────────────────────── + private const string AnalysisRules = """ + - Use only provided input sections as evidence. + - Do not invent fields, documents, requirements, or facts. + - Prefer, in order: direct evidence from DATA, specific supporting evidence from ATTACHMENTS, then broader context only when necessary. + - Treat missing or empty values as findings only when they weaken rubric evidence. + - Prefer material findings; avoid nitpicking. + - Do not restate basic application facts as findings unless they support a specific reviewer conclusion about readiness, feasibility, budget credibility, eligibility, or confidence in proceeding. + - Prefer direct evidence from DATA over derivative statements in ATTACHMENTS when both address the same point. + - If ATTACHMENTS evidence is used, cite the attachment by name in detail. + - Each detail must cite concrete evidence from DATA or ATTACHMENTS. + - Write reviewer-facing natural language. Do not refer to prompt section names, internal field keys, or schema labels such as DATA, ATTACHMENTS, ProjectSummary, CustomField1, or OrganizationType. + - Refer to evidence by its plain-language meaning, quoted text, or attachment name rather than internal key names. + - Only include warnings when the evidence shows a specific, concrete risk, inconsistency, or meaningful uncertainty; a stated risk label alone is not enough. + - Do not state that one amount exceeds, matches, or conflicts with another unless the comparison is directly supported by the provided values. + - Do not treat ordinary lack of detailed supporting explanation as a material gap unless the provided evidence creates real uncertainty about feasibility, eligibility, or budget credibility. + - Prefer neutral evidence descriptions over evaluative adjectives unless the evidence directly supports a strong conclusion. + - Do not describe capacity, feasibility, or justification as strong, detailed, or well-supported unless the evidence shows more than the existence of basic organizational, budget, or timeline information. + - Do not infer community support, established partnerships, or delivery capacity from a single partner reference, staff count, or basic organizational status alone. + - Do not describe a timeline as realistic or feasible based only on start and end dates unless additional evidence supports deliverability. + - Use 3-6 words for title. + - Summary titles should name the specific substantive reviewer conclusion, strength, or risk, not a generic evaluation label or abstract category. + - Each detail must be 1-2 complete sentences. + - Summaries and nextSteps must be concrete, distinct, reviewer-relevant, and specific to this application's evidence. + - Avoid generic praise, checklist language, and repeated conclusions across lists. + - Do not use a summary merely to say that supporting documents were provided; summarize the specific substantive evidence they add, or omit the finding. + - If no findings exist, return empty arrays. + - Rating must be HIGH, MEDIUM, or LOW. + - Use summaries for overall application quality/readiness synthesis. + - Use nextSteps for concrete reviewer-facing next actions based on the provided evidence. + - nextSteps may include proceeding with the normal review process when the application appears ready for that step. + - When evidence shows a meaningful gap, inconsistency, or uncertainty, use nextSteps for specific follow-up or verification actions. + - Return an empty array only when no concrete next action would help the reviewer. + - recommendation.decision must be PROCEED or HOLD. + - Use HOLD only when provided evidence shows a material eligibility, feasibility, budget, or readiness concern that would reasonably block scoring or decision-making. + - recommendation.rationale must explain the high-level recommendation in 1-2 complete sentences using provided evidence. + - recommendation.rationale should name the 1-3 strongest evidence-based reasons for the recommendation. + """; + + // ── v0/attachment.system.txt ───────────────────────────────────────────── + private const string AttachmentSystemV0 = """ + You are a professional grant analyst for the BC Government. + + Please analyze this attachment and provide a concise reviewer-facing summary of its content, purpose, and key information. + + OUTPUT + { + "summary": "" + } + + Use only ATTACHMENT as evidence. If ATTACHMENT.text is present, summarize actual content; otherwise provide a conservative file-level summary. Write 1-2 complete sentences and return valid plain JSON only in the exact OUTPUT shape. + """; + + // ── v0/attachment.user.txt ─────────────────────────────────────────────── + private const string AttachmentUserV0 = """ + ATTACHMENT + {{ATTACHMENT}} + """; + + // ── v1/attachment.system.txt ───────────────────────────────────────────── + private const string AttachmentSystemV1 = """ + ROLE + You are a careful grant review assistant for human reviewers. Do not fill gaps, assume compliance, or treat relevance as proof. + + TASK + Using ATTACHMENT, OUTPUT, and RULES: + 1. Review the attachment to identify what it contains. + 2. Summarize the attachment itself, not the overall project. + 3. Return a concise reviewer-facing summary. + """; + + // ── v1/attachment.user.txt ─────────────────────────────────────────────── + private const string AttachmentUserV1 = """ + ATTACHMENT + {{ATTACHMENT}} + + OUTPUT + {{OUTPUT}} + + RULES + {{RULES}} + {{COMMON_RULES}} + """; + + // ── v1/attachment.output.txt ───────────────────────────────────────────── + private const string AttachmentOutput = """ + { + "summary": "" + } + """; + + // ── v1/attachment.rules.txt ────────────────────────────────────────────── + private const string AttachmentRules = """ + - Use only ATTACHMENT as evidence. + - Summarize actual content when ATTACHMENT.text is present; otherwise provide a conservative file-level summary. + - Describe the attachment itself rather than summarizing the overall project. + - Ensure the summary describes the attachment itself, not the overall project. + - If ATTACHMENT.text is primarily structured application, contact, organization, budget, or date fields, summarize it as a metadata-style attachment rather than rewriting it as a generic project summary. + - Begin with what the attachment contains or provides, not the file name or file type, unless that metadata is necessary to describe the evidence. + - Do not invent missing details. + - Do not calculate or restate totals, sums, or aggregates unless they are explicitly present in ATTACHMENT.text. + - Write reviewer-facing natural language. Do not refer to prompt section names, internal field keys, or schema labels such as ATTACHMENT or ATTACHMENT.text. + - Refer to evidence by its plain-language meaning, quoted text, or file name rather than internal key names. + - Write 1-2 complete sentences. + - Summary must be grounded in concrete ATTACHMENT evidence. + - Return exactly one object with only the key: summary. + """; + + // ── v0/scoresheet.system.txt ───────────────────────────────────────────── + private const string ScoresheetSystemV0 = """ + You are an expert grant application reviewer for the BC Government. + Analyze the provided application and answer only the questions in the specified scoresheet section. + Be thorough, objective, and fair. Base answers strictly on provided evidence. + Always provide evidence-grounded rationale and an honest confidence score. + Respond only with valid JSON in the exact format requested. + """; + + // ── v0/scoresheet.user.txt ─────────────────────────────────────────────── + private const string ScoresheetUserV0 = """ + APPLICATION CONTENT: + {{DATA}} + + ATTACHMENT SUMMARIES: + {{ATTACHMENTS}} + + SCORESHEET SECTION: + {{SECTION}} + + RESPONSE TEMPLATE: + {{RESPONSE}} + + Please analyze this grant application and provide answers for each question in the specified section only. + + For each question, provide: + 1. The answer based on the application evidence + 2. A brief rationale (1-2 complete sentences) citing concrete supporting evidence + 3. A confidence score from 0-100 (integer) indicating certainty in the selected answer + + OUTPUT + { + "": { + "answer": "", + "rationale": "", + "confidence": + } + } + + Important: + - Use only APPLICATION CONTENT and ATTACHMENT SUMMARIES as evidence. + - Answer only the question IDs in the specified section. + - Every question must include answer, rationale, and confidence. + - Use RESPONSE TEMPLATE as the contract and fill every placeholder value. + - answer type must match the question type. + - For select list questions, return only the option number as a string, never label text. + - rationale must be 1-2 complete sentences grounded in evidence. + - confidence must be an integer from 0 to 100 in increments of 5. + - Return valid plain JSON only in the exact OUTPUT shape. + """; + + // ── v1/scoresheet.system.txt ───────────────────────────────────────────── + private const string ScoresheetSystemV1 = """ + ROLE + You are a careful grant review assistant for human reviewers. Do not fill gaps, assume compliance, or treat relevance as proof. + + TASK + Using DATA, ATTACHMENTS, SECTION, RESPONSE, OUTPUT, and RULES: + 1. Review each question in SECTION one at a time. + 2. Identify the exact condition the question asks about. + 3. Consider only the most relevant evidence in DATA and ATTACHMENTS for that condition. + 4. Choose the most conservative valid answer supported by that evidence. + 5. If evidence is incomplete or indirect, explain the uncertainty in the rationale. + 6. Repeat for every question in SECTION. + """; + + // ── v1/scoresheet.user.txt ─────────────────────────────────────────────── + private const string ScoresheetUserV1 = """ + DATA + {{DATA}} + + ATTACHMENTS + {{ATTACHMENTS}} + + SECTION + {{SECTION}} + + RESPONSE + {{RESPONSE}} + + OUTPUT + {{OUTPUT}} + + RULES + {{RULES}} + {{COMMON_RULES}} + """; + + // ── v1/scoresheet.output.txt ───────────────────────────────────────────── + private const string ScoresheetOutput = """ + { + "": { + "answer": "", + "rationale": "", + "confidence": + } + } + """; + + // ── v1/scoresheet.rules.txt ────────────────────────────────────────────── + private const string ScoresheetRules = """ + - Use only DATA and ATTACHMENTS as evidence. + - Do not invent missing application details. + - Ignore fields or details that are not relevant to the specific question being answered. + - Prefer, in order: direct evidence of the exact condition asked, closely related supporting evidence, then general context only when necessary. + - If evidence is insufficient, partial, indirect, missing, or non-specific, choose the most conservative valid answer and explain the uncertainty. + - Do not convert general project descriptions into evidence for a specific scored condition unless that condition is directly supported. + - Treat prefilled labels, ratings, rankings, or statuses as background context only unless the question explicitly asks for that same item. + - Do not treat related concepts as equivalent; answer the specific question asked, not a nearby concept. + - Do not infer unsupported claims about requirements, conditions, relationships, compliance elements, mitigations, supports, or outcomes. + - Answer a specific condition positively only when that exact condition is directly evidenced in DATA or ATTACHMENTS. + - For eligibility, completeness, ownership, location, or compliance questions, do not answer positively unless the exact condition is directly confirmed in the provided evidence. + - If the evidence shows only involvement, presence, relevance, or association, do not treat that alone as proof that a requirement or condition is satisfied. + - Return exactly one answer object per question ID in SECTION.questions. + - Do not omit any question IDs from SECTION.questions. + - Do not add keys that are not question IDs from SECTION.questions. + - Use the exact question IDs from RESPONSE and SECTION.questions without alteration; never rewrite, normalize, or regenerate a question ID. + - Use RESPONSE as the output contract and fill every placeholder value. + - Each answer object must include: "answer", "rationale", and "confidence". + - Never omit "answer", "rationale", or "confidence" for any question type. + - The "answer" value type must match question type: Number => numeric; YesNo/SelectList/Text/TextArea => string. + """; + + // ── v1/common.rules.txt ────────────────────────────────────────────────── + private const string CommonRules = """ + - Any narrative text response must be at least 12 words. + - Return values exactly as specified in OUTPUT. + - Do not return keys outside OUTPUT. + - Return valid JSON only. + - Return plain JSON only (no markdown). + """; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIPrompt.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIPrompt.cs new file mode 100644 index 0000000000..8aeb74d3d2 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIPrompt.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Unity.AI.Domain; + +public class AIPrompt : AuditedAggregateRoot, IMultiTenant +{ + public virtual Guid? TenantId { get; protected set; } + + public string Name { get; set; } = default!; + + public string? Description { get; set; } + + public PromptType Type { get; set; } + + public bool IsActive { get; set; } = true; + + public ICollection Versions { get; set; } = new List(); + + protected AIPrompt() { } + + public AIPrompt(Guid id, string name, PromptType type, Guid? tenantId = null) + { + Id = id; + Name = name; + Type = type; + TenantId = tenantId; + IsActive = true; + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIPromptVersion.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIPromptVersion.cs new file mode 100644 index 0000000000..440aec6e0c --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Domain/AIPromptVersion.cs @@ -0,0 +1,50 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Unity.AI.Domain; + +public class AIPromptVersion : AuditedAggregateRoot, IMultiTenant +{ + public virtual Guid? TenantId { get; protected set; } + + public Guid PromptId { get; set; } + public AIPrompt? Prompt { get; set; } + + public int VersionNumber { get; set; } + + public string SystemPrompt { get; set; } = default!; + public string UserPromptTemplate { get; set; } = default!; + public string? DeveloperNotes { get; set; } + + public string? TargetModel { get; set; } + public string? TargetProvider { get; set; } + + public double Temperature { get; set; } = 0.2; + public int? MaxTokens { get; set; } + + public bool IsPublished { get; set; } + public bool IsDeprecated { get; set; } + + /// Optional JSON metadata for extensibility (stored as Postgres jsonb). + public string? MetadataJson { get; set; } + + protected AIPromptVersion() { } + + public AIPromptVersion( + Guid id, + Guid promptId, + int versionNumber, + string systemPrompt, + string userPromptTemplate, + Guid? tenantId = null) + { + Id = id; + PromptId = promptId; + VersionNumber = versionNumber; + SystemPrompt = systemPrompt; + UserPromptTemplate = userPromptTemplate; + TenantId = tenantId; + Temperature = 0.2; + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs index bfdb1bb031..94ccefb672 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/EntityFrameworkCore/AIDbContextModelCreatingExtensions.cs @@ -1,5 +1,7 @@ using Microsoft.EntityFrameworkCore; +using Unity.AI.Domain; using Volo.Abp; +using Volo.Abp.EntityFrameworkCore.Modeling; namespace Unity.AI.EntityFrameworkCore; @@ -9,8 +11,48 @@ public static void ConfigureAI(this ModelBuilder modelBuilder) { Check.NotNull(modelBuilder, nameof(modelBuilder)); - // Configure AI entities here as they are introduced. - // Example: modelBuilder add Entity To table and configurations + modelBuilder.Entity(b => + { + b.ToTable(AIDbProperties.DbTablePrefix + "AIPrompts", AIDbProperties.DbSchema); + b.ConfigureByConvention(); + + b.Property(x => x.Name) + .IsRequired() + .HasMaxLength(200); + + b.Property(x => x.Description) + .HasMaxLength(2000); + + b.Property(x => x.Type) + .IsRequired(); + + b.HasMany(x => x.Versions) + .WithOne(x => x.Prompt) + .HasForeignKey(x => x.PromptId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(b => + { + b.ToTable(AIDbProperties.DbTablePrefix + "AIPromptVersions", AIDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.SystemPrompt).IsRequired().HasColumnType("text"); + b.Property(x => x.UserPromptTemplate).IsRequired().HasColumnType("text"); + + b.Property(x => x.TargetModel) + .HasMaxLength(100); + + b.Property(x => x.TargetProvider) + .HasMaxLength(100); + + b.Property(x => x.MetadataJson) + .HasColumnType("jsonb"); + + b.HasIndex(x => new { x.PromptId, x.VersionNumber }) + .IsUnique(); + }); } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptAppService.cs new file mode 100644 index 0000000000..5167d1b136 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptAppService.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Authorization; +using System; +using System.Threading.Tasks; +using Unity.AI.Domain; +using Unity.Modules.Shared.Permissions; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace Unity.AI.Prompts; + +[Authorize(IdentityConsts.ITOperationsPolicyName)] +public class AIPromptAppService : + CrudAppService< + AIPrompt, + AIPromptDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateAIPromptDto>, + IAIPromptAppService +{ + public AIPromptAppService(IRepository repository) + : base(repository) + { + GetPolicyName = IdentityConsts.ITOperationsPolicyName; + GetListPolicyName = IdentityConsts.ITOperationsPolicyName; + CreatePolicyName = IdentityConsts.ITOperationsPolicyName; + UpdatePolicyName = IdentityConsts.ITOperationsPolicyName; + DeletePolicyName = IdentityConsts.ITOperationsPolicyName; + } + + public override async Task GetAsync(Guid id) + { + using (CurrentTenant.Change(null)) + { + return await base.GetAsync(id); + } + } + + public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) + { + using (CurrentTenant.Change(null)) + { + return await base.GetListAsync(input); + } + } + + public override async Task CreateAsync(CreateUpdateAIPromptDto input) + { + using (CurrentTenant.Change(null)) + { + return await base.CreateAsync(input); + } + } + + public override async Task UpdateAsync(Guid id, CreateUpdateAIPromptDto input) + { + using (CurrentTenant.Change(null)) + { + return await base.UpdateAsync(id, input); + } + } + + public override async Task DeleteAsync(Guid id) + { + using (CurrentTenant.Change(null)) + { + await base.DeleteAsync(id); + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptVersionAppService.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptVersionAppService.cs new file mode 100644 index 0000000000..a44218d410 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Application/Prompts/AIPromptVersionAppService.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.Authorization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.AI.Domain; +using Unity.Modules.Shared.Permissions; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace Unity.AI.Prompts; + +[Authorize(IdentityConsts.ITOperationsPolicyName)] +public class AIPromptVersionAppService : + CrudAppService< + AIPromptVersion, + AIPromptVersionDto, + Guid, + PagedAndSortedResultRequestDto, + CreateUpdateAIPromptVersionDto>, + IAIPromptVersionAppService +{ + public AIPromptVersionAppService(IRepository repository) + : base(repository) + { + GetPolicyName = IdentityConsts.ITOperationsPolicyName; + GetListPolicyName = IdentityConsts.ITOperationsPolicyName; + CreatePolicyName = IdentityConsts.ITOperationsPolicyName; + UpdatePolicyName = IdentityConsts.ITOperationsPolicyName; + DeletePolicyName = IdentityConsts.ITOperationsPolicyName; + } + + public async Task> GetByPromptAsync(Guid promptId) + { + using (CurrentTenant.Change(null)) + { + var items = await Repository.GetListAsync(v => v.PromptId == promptId); + var sorted = items.OrderBy(v => v.VersionNumber).ToList(); + return new ListResultDto( + ObjectMapper.Map, List>(sorted)); + } + } + + public override async Task GetAsync(Guid id) + { + using (CurrentTenant.Change(null)) + { + return await base.GetAsync(id); + } + } + + public override async Task> GetListAsync(PagedAndSortedResultRequestDto input) + { + using (CurrentTenant.Change(null)) + { + return await base.GetListAsync(input); + } + } + + public override async Task CreateAsync(CreateUpdateAIPromptVersionDto input) + { + using (CurrentTenant.Change(null)) + { + return await base.CreateAsync(input); + } + } + + public override async Task UpdateAsync(Guid id, CreateUpdateAIPromptVersionDto input) + { + using (CurrentTenant.Change(null)) + { + return await base.UpdateAsync(id, input); + } + } + + public override async Task DeleteAsync(Guid id) + { + using (CurrentTenant.Change(null)) + { + await base.DeleteAsync(id); + } + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json index f660d259d3..b4244f5390 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/Localization/AI/en.json @@ -5,6 +5,28 @@ "Permission:AI.Reporting": "AI Reporting", "Permission:AI.ApplicationAnalysis": "AI Application Analysis", "Permission:AI.AttachmentSummary": "AI Attachment Summary", - "Permission:AI.ScoringAssistant": "AI Scoring Assistant" + "Permission:AI.ScoringAssistant": "AI Scoring Assistant", + "Permission:AI.Prompts": "AI Prompt Management", + "Permission:AI.Prompts.Create": "Create Prompts", + "Permission:AI.Prompts.Update": "Edit Prompts", + "Permission:AI.Prompts.Delete": "Delete Prompts", + "AIPrompts": "AI Prompts", + "AIPrompt": "AI Prompt", + "AIPromptVersion": "Prompt Version", + "AIPromptVersions": "Prompt Versions", + "PromptType": "Type", + "PromptName": "Name", + "PromptDescription": "Description", + "PromptIsActive": "Active", + "VersionNumber": "Version Number", + "SystemPrompt": "System Prompt", + "UserPromptTemplate": "User Prompt Template", + "DeveloperNotes": "Developer Notes", + "TargetModel": "Target Model", + "TargetProvider": "Target Provider", + "Temperature": "Temperature", + "MaxTokens": "Max Tokens", + "IsPublished": "Published", + "IsDeprecated": "Deprecated" } } diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/PromptType.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/PromptType.cs new file mode 100644 index 0000000000..9b57dc1e75 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Domain.Shared/PromptType.cs @@ -0,0 +1,10 @@ +namespace Unity.AI; + +/// Categorises what role an AI prompt plays in the system. +public enum PromptType +{ + Orchestrator = 0, + Skill = 1, + Instruction = 2, + Agent = 3 +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebModule.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebModule.cs index e82f53daa7..cda63f400a 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebModule.cs +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/AIWebModule.cs @@ -1,9 +1,11 @@ using Microsoft.Extensions.DependencyInjection; using Unity.AI.Localization; +using Unity.AI.Web.Menus; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AutoMapper; using Volo.Abp.Modularity; +using Volo.Abp.UI.Navigation; using Volo.Abp.VirtualFileSystem; namespace Unity.AI.Web; @@ -35,6 +37,11 @@ public override void ConfigureServices(ServiceConfigurationContext context) options.FileSets.AddEmbedded(); }); + Configure(options => + { + options.MenuContributors.Add(new AIMenuContributor()); + }); + context.Services.AddAutoMapperObjectMapper(); Configure(options => { diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs new file mode 100644 index 0000000000..453ad3549a --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenuContributor.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Unity.Modules.Shared.Permissions; +using Volo.Abp.UI.Navigation; + +namespace Unity.AI.Web.Menus; + +public class AIMenuContributor : IMenuContributor +{ + public async Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name == StandardMenus.Main) + { + await ConfigureMainMenuAsync(context); + } + } + + private static Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + context.Menu.AddItem(new ApplicationMenuItem( + name: AIMenus.Prompts, + displayName: "AI Prompts", + url: "~/Prompts", + icon: "fl fl-ai-prompts", + order: 900, + requiredPermissionName: IdentityConsts.ITOperationsPermissionName + )); + + return Task.CompletedTask; + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenus.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenus.cs new file mode 100644 index 0000000000..f93517efe0 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Menus/AIMenus.cs @@ -0,0 +1,8 @@ +namespace Unity.AI.Web.Menus; + +public static class AIMenus +{ + private const string Prefix = "AI"; + + public const string Prompts = Prefix + ".Prompts"; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/CreateModal.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/CreateModal.cshtml new file mode 100644 index 0000000000..ddbdeb5bae --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/CreateModal.cshtml @@ -0,0 +1,23 @@ +@page +@using Unity.AI.Localization +@using Unity.AI.Web.Pages.Prompts +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model CreateModalModel +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/CreateModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/CreateModal.cshtml.cs new file mode 100644 index 0000000000..25a3b31c8b --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/CreateModal.cshtml.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Unity.AI.Prompts; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.AI.Web.Pages.Prompts; + +public class CreateModalModel : AbpPageModel +{ + [BindProperty] + public CreateUpdateAIPromptDto Prompt { get; set; } = new(); + + private readonly IAIPromptAppService _promptAppService; + + public CreateModalModel(IAIPromptAppService promptAppService) + { + _promptAppService = promptAppService; + } + + public void OnGet() + { + Prompt = new CreateUpdateAIPromptDto { IsActive = true }; + } + + public async Task OnPostAsync() + { + await _promptAppService.CreateAsync(Prompt); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/EditModal.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/EditModal.cshtml new file mode 100644 index 0000000000..5ad9cff4a4 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/EditModal.cshtml @@ -0,0 +1,37 @@ +@page +@using Unity.AI.Localization +@using Unity.AI.Web.Pages.Prompts +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model EditModalModel +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + + + + + + + +
+ +
+
+
@L["AIPromptVersions"]
+ +
+ +
diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/EditModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/EditModal.cshtml.cs new file mode 100644 index 0000000000..1279b7a691 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/EditModal.cshtml.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.AI.Prompts; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.AI.Web.Pages.Prompts; + +public class EditModalModel : AbpPageModel +{ + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public CreateUpdateAIPromptDto Prompt { get; set; } = new(); + + private readonly IAIPromptAppService _promptAppService; + + public EditModalModel(IAIPromptAppService promptAppService) + { + _promptAppService = promptAppService; + } + + public async Task OnGetAsync() + { + var dto = await _promptAppService.GetAsync(Id); + Prompt = new CreateUpdateAIPromptDto + { + Name = dto.Name, + Description = dto.Description, + Type = dto.Type, + IsActive = dto.IsActive + }; + } + + public async Task OnPostAsync() + { + await _promptAppService.UpdateAsync(Id, Prompt); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml new file mode 100644 index 0000000000..0099f5cf0e --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml @@ -0,0 +1,137 @@ +@page +@using Unity.AI.Localization +@using Unity.AI.Web.Pages.Prompts +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Layout +@model IndexModel +@inject IStringLocalizer L +@inject IPageLayout PageLayout +@{ + PageLayout.Content.BreadCrumb.Add(L["AIPrompts"].Value); +} +@section styles { + +} +@section scripts { + +} + +
+
+
+

@L["AIPrompts"]

+
+
+ +
+
+
+ +
+ + +
+
+
+ +
+
+
+ + +
+ + + + +
+
diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml.cs new file mode 100644 index 0000000000..e24584432f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.cshtml.cs @@ -0,0 +1,8 @@ +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.AI.Web.Pages.Prompts; + +public class IndexModel : AbpPageModel +{ + +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.css b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.css new file mode 100644 index 0000000000..ffddbdf1e6 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.css @@ -0,0 +1,122 @@ +/* ── Split-pane container ─────────────────────────────────────────────────── */ +.prompts-split-container { + display: flex; + flex-direction: row; + align-items: flex-start; + min-height: 400px; + gap: 0; + /* No fixed height — lets the DataTable footer and page footer remain visible */ +} + +/* ── Left pane ────────────────────────────────────────────────────────────── */ +.prompts-left-pane { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + transition: none; +} + +/* ── Drag divider ─────────────────────────────────────────────────────────── */ +.prompts-split-divider { + display: none; + width: 7px; + flex-shrink: 0; + cursor: col-resize; + background: #dee2e6; + border-left: 1px solid #ced4da; + border-right: 1px solid #ced4da; + position: sticky; + top: 10px; + height: calc(100vh - 190px); + z-index: 10; + transition: background 0.15s; +} + +.prompts-split-divider::after { + content: '⋮'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #868e96; + font-size: 20px; + line-height: 1; + pointer-events: none; +} + +.prompts-split-divider:hover, +.prompts-split-divider.dragging { + background: #adb5bd; +} + +.prompts-split-container.split-active .prompts-split-divider { + display: flex; + align-items: center; + justify-content: center; +} + +/* ── Right pane ───────────────────────────────────────────────────────────── */ +.prompts-right-pane { + flex-shrink: 0; + width: 52%; + min-width: 300px; + max-width: 80%; + overflow: hidden; + display: flex; + flex-direction: column; + /* Sticky so the editor stays visible as the left-pane table grows */ + position: sticky; + top: 10px; + /* Explicit height (not max-height) so h-100 on the inner card resolves correctly */ + height: calc(100vh - 190px); +} + +/* ── Version editor header ────────────────────────────────────────────────── */ +.version-editor-header { + background: #f8f9fa; + flex-shrink: 0; +} + +.version-select { + min-width: 160px; + max-width: 220px; +} + +/* ── Version editor form body ─────────────────────────────────────────────── */ +.version-editor-body { + flex: 1 1 0; /* 0 basis so flex shrinks below content natural size */ + min-height: 0; /* required for overflow-y:auto to work inside a flex column */ + overflow-y: auto; +} + +/* ── Resizable textareas ──────────────────────────────────────────────────── */ +.version-textarea { + resize: vertical; + min-height: 72px; + font-size: 0.82rem; + line-height: 1.5; +} + +.version-textarea--code { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; +} + +.version-textarea--json { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; + min-height: 120px; +} + +/* ── Row selection ────────────────────────────────────────────────────────── */ +#AIPromptsTable tbody tr { + cursor: pointer; +} + +#AIPromptsTable tbody tr.prompt-selected td { + background-color: #cfe2ff !important; +} + +/* ── Drag ghost (prevent text selection while dragging) ───────────────────── */ +body.split-dragging { + cursor: col-resize !important; + user-select: none !important; +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js new file mode 100644 index 0000000000..28062f58cf --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Index.js @@ -0,0 +1,380 @@ +$(function () { + const l = abp.localization.getResource('AI'); + + // Prompt-level modals (create / edit prompt metadata only) + let createModal = new abp.ModalManager(abp.appPath + 'Prompts/CreateModal'); + let editModal = new abp.ModalManager(abp.appPath + 'Prompts/EditModal'); + + // ── State ──────────────────────────────────────────────────────────────── + let currentVersionId = null; + let isNewVersion = false; + let cachedVersions = []; // versions for the currently-selected prompt + + // ── Table columns ──────────────────────────────────────────────────────── + const listColumns = [ + { + title: l('PromptName'), + name: 'name', + data: 'name', + index: 0 + }, + { + title: l('PromptType'), + name: 'type', + data: 'type', + index: 1, + render: (data) => { + const types = ['Orchestrator', 'Skill', 'Instruction', 'Agent']; + return types[data] ?? data; + } + }, + { + title: l('PromptDescription'), + name: 'description', + data: 'description', + index: 2, + defaultContent: '' + }, + { + title: l('PromptIsActive'), + name: 'isActive', + data: 'isActive', + index: 3, + render: (data) => data + ? 'Active' + : 'Inactive' + }, + { + title: l('Actions'), + data: 'id', + orderable: false, + className: 'text-center', + name: 'rowActions', + index: 4, + rowAction: { + items: [ + { + text: 'Edit Prompt', + action: (data) => editModal.open({ id: data.record.id }) + } + ] + } + } + ]; + + const responseCallback = (result) => ({ + recordsTotal: result.totalCount, + recordsFiltered: result.items.length, + data: result.items + }); + + const actionButtons = [ + { + text: ' New Prompt', + titleAttr: 'New Prompt', + id: 'CreatePromptButton', + className: 'btn btn-light rounded-1', + action: (e) => { e.preventDefault(); createModal.open(); } + } + ]; + + const defaultVisibleColumns = ['name', 'type', 'description', 'isActive', 'rowActions']; + const dt = $('#AIPromptsTable'); + + const dataTable = initializeDataTable({ + dt, + defaultVisibleColumns, + listColumns, + maxRowsPerPage: 25, + defaultSortColumn: 0, + dataEndpoint: unity.aI.prompts.aIPrompt.getList, + data: {}, + responseCallback, + actionButtons, + pagingEnabled: true, + reorderEnabled: false, + languageSetValues: {}, + dataTableName: 'AIPromptsTable', + dynamicButtonContainerId: 'dynamicButtonContainerId', + useNullPlaceholder: true, + externalSearchId: 'search-prompts', + fixedHeaders: true + }); + + createModal.onResult(() => dataTable.ajax.reload()); + editModal.onResult(() => dataTable.ajax.reload()); + + // ── Row click → open version panel ────────────────────────────────────── + $('#AIPromptsTable').on('click', 'tbody tr', function (e) { + // Don't intercept action-column clicks + if ($(e.target).closest('.dropdown, .dropdown-menu, button, a').length) return; + + const rowData = dataTable.row(this).data(); + if (!rowData) return; + + // Highlight selected row + $('#AIPromptsTable tbody tr').removeClass('prompt-selected'); + $(this).addClass('prompt-selected'); + + openVersionPanel(rowData); + }); + + // ── Open / populate right panel ────────────────────────────────────────── + function openVersionPanel(promptData) { + $('#versionEditorTitle').text(promptData.name); + $('#versionPromptId').val(promptData.id); + + // Activate split layout + $('#promptsSplitContainer').addClass('split-active'); + $('#promptsRightPane').show(); + + loadVersions(promptData.id); + } + + function loadVersions(promptId) { + unity.aI.prompts.aIPromptVersion.getByPrompt(promptId).then(function (result) { + cachedVersions = result.items || []; + const $select = $('#versionSelect'); + $select.empty(); + + if (cachedVersions.length === 0) { + $select.append(''); + clearVersionForm(0); + return; + } + + // Sort ascending by versionNumber in place so [last] is always the max + cachedVersions.sort((a, b) => a.versionNumber - b.versionNumber); + + cachedVersions.forEach(function (v) { + $select.append(``); + }); + + // Select latest (highest versionNumber) by default + const latest = cachedVersions[cachedVersions.length - 1]; + $select.val(latest.id); + populateVersionForm(latest); + }); + } + + // Version dropdown change + $('#versionSelect').on('change', function () { + const id = $(this).val(); + if (!id) return; + const v = cachedVersions.find(x => x.id === id); + if (v) { + populateVersionForm(v); + } else { + // fallback: fetch from server + unity.aI.prompts.aIPromptVersion.get(id).then(populateVersionForm); + } + }); + + // ── Populate form from a version DTO ───────────────────────────────────── + function populateVersionForm(v) { + isNewVersion = false; + currentVersionId = v.id; + + $('#versionId').val(v.id); + $('#versionNumber').val(v.versionNumber); + $('#versionTargetModel').val(v.targetModel ?? ''); + $('#versionTargetProvider').val(v.targetProvider ?? ''); + $('#versionTemperature').val(v.temperature ?? 0.2); + $('#versionMaxTokens').val(v.maxTokens ?? ''); + $('#versionIsPublished').prop('checked', v.isPublished ?? false); + $('#versionIsDeprecated').prop('checked', v.isDeprecated ?? false); + $('#versionSystemPrompt').val(v.systemPrompt ?? '').removeClass('is-invalid'); + $('#versionUserPromptTemplate').val(v.userPromptTemplate ?? '').removeClass('is-invalid'); + $('#versionDeveloperNotes').val(v.developerNotes ?? ''); + + // Pretty-print MetadataJson if valid + let meta = v.metadataJson ?? ''; + if (meta) { + try { meta = JSON.stringify(JSON.parse(meta), null, 2); } catch (e) { console.warn('MetadataJson is not valid JSON; displaying raw value.', e); } + } + $('#versionMetadataJson').val(meta); + + clearJsonError(); + $('#saveVersionBtnLabel').text('Save Version'); + } + + // ── Clear form for a brand-new version ─────────────────────────────────── + function clearVersionForm(nextVersionNumber) { + isNewVersion = true; + currentVersionId = null; + + $('#versionId').val(''); + $('#versionTargetModel').val(''); + $('#versionTargetProvider').val(''); + $('#versionTemperature').val(0.2); + $('#versionMaxTokens').val(''); + $('#versionIsPublished').prop('checked', false); + $('#versionIsDeprecated').prop('checked', false); + $('#versionSystemPrompt').val(''); + $('#versionUserPromptTemplate').val(''); + $('#versionDeveloperNotes').val(''); + $('#versionMetadataJson').val(''); + + clearJsonError(); + $('#saveVersionBtnLabel').text('Create Version'); + } + + // ── New version button ──────────────────────────────────────────────────── + $('#newVersionBtn').on('click', function () { + const maxNum = cachedVersions.reduce((max, v) => Math.max(max, v.versionNumber), -1); + const next = maxNum + 1; + + // Add a placeholder option + $('#versionSelect option[data-new]').remove(); + $('#versionSelect').prepend( + `` + ); + + clearVersionForm(next); + }); + + // ── Save / create version ───────────────────────────────────────────────── + $('#saveVersionBtn').on('click', function () { + const promptId = $('#versionPromptId').val(); + if (!promptId) return; + + // Required-field validation + const systemPrompt = $('#versionSystemPrompt').val().trim(); + const userPromptTemplate = $('#versionUserPromptTemplate').val().trim(); + let valid = true; + if (systemPrompt) { + $('#versionSystemPrompt').removeClass('is-invalid'); + } else { + $('#versionSystemPrompt').addClass('is-invalid'); + valid = false; + } + if (userPromptTemplate) { + $('#versionUserPromptTemplate').removeClass('is-invalid'); + } else { + $('#versionUserPromptTemplate').addClass('is-invalid'); + valid = false; + } + if (!valid) { + $('#versionSystemPrompt.is-invalid, #versionUserPromptTemplate.is-invalid')[0]?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + return; + } + + const metaRaw = $('#versionMetadataJson').val().trim(); + if (metaRaw && !validateJson(metaRaw)) return; + + const dto = { + promptId: promptId, + versionNumber: Number.parseInt($('#versionNumber').val()) || 0, + systemPrompt: systemPrompt, + userPromptTemplate: userPromptTemplate, + developerNotes: $('#versionDeveloperNotes').val() || null, + targetModel: $('#versionTargetModel').val() || null, + targetProvider: $('#versionTargetProvider').val() || null, + temperature: Number.parseFloat($('#versionTemperature').val()) || 0.2, + maxTokens: $('#versionMaxTokens').val() ? Number.parseInt($('#versionMaxTokens').val()) : null, + isPublished: $('#versionIsPublished').is(':checked'), + isDeprecated: $('#versionIsDeprecated').is(':checked'), + metadataJson: metaRaw || null + }; + + if (isNewVersion) { + const newOpt = $('#versionSelect option[data-new]'); + dto.versionNumber = newOpt.length ? Number.parseInt(newOpt.data('num')) : 0; + + unity.aI.prompts.aIPromptVersion.create(dto) + .then(function () { + abp.notify.success('Version created'); + loadVersions(promptId); + }) + .catch(function (err) { + abp.notify.error(err?.message || 'Failed to create version'); + }); + } else { + unity.aI.prompts.aIPromptVersion.update(currentVersionId, dto) + .then(function () { + abp.notify.success('Version saved'); + loadVersions(promptId); + }) + .catch(function (err) { + abp.notify.error(err?.message || 'Failed to save version'); + }); + } + }); + + // ── Format JSON button ──────────────────────────────────────────────────── + $('#formatJsonBtn').on('click', function () { + const raw = $('#versionMetadataJson').val().trim(); + if (raw && validateJson(raw)) { + $('#versionMetadataJson').val(JSON.stringify(JSON.parse(raw), null, 2)); + } + }); + + // ── JSON validation helper ──────────────────────────────────────────────── + function validateJson(str) { + try { + JSON.parse(str); + clearJsonError(); + return true; + } catch (e) { + $('#jsonValidationMsg').text('Invalid JSON: ' + e.message).show(); + $('#versionMetadataJson').addClass('is-invalid'); + return false; + } + } + + function clearJsonError() { + $('#jsonValidationMsg').hide().text(''); + $('#versionMetadataJson').removeClass('is-invalid'); + } + + $('#versionMetadataJson').on('input', function () { + const raw = $(this).val().trim(); + if (raw) validateJson(raw); else clearJsonError(); + }); + + // ── Draggable divider ───────────────────────────────────────────────────── + const $divider = $('#promptsDivider'); + const $leftPane = $('#promptsLeftPane'); + const $rightPane = $('#promptsRightPane'); + const $container = $('#promptsSplitContainer'); + + let isDragging = false; + let dragStartX = 0; + let dragStartLeft = 0; + + $divider.on('mousedown', function (e) { + isDragging = true; + dragStartX = e.clientX; + dragStartLeft = $leftPane.width(); + $divider.addClass('dragging'); + $('body').addClass('split-dragging'); + e.preventDefault(); + }); + + $(document).on('mousemove.splitDrag', function (e) { + if (!isDragging) return; + + const totalWidth = $container.width(); + const dividerW = $divider.outerWidth(); + const delta = e.clientX - dragStartX; + let newLeft = dragStartLeft + delta; + const minLeft = totalWidth * 0.2; + const maxLeft = totalWidth * 0.8 - dividerW; + + newLeft = Math.max(minLeft, Math.min(maxLeft, newLeft)); + + const leftPct = (newLeft / totalWidth * 100).toFixed(2); + const rightPct = ((totalWidth - newLeft - dividerW) / totalWidth * 100).toFixed(2); + + $leftPane.css('flex', `0 0 ${leftPct}%`); + $rightPane.css({ 'flex': 'none', 'width': rightPct + '%' }); + }); + + $(document).on('mouseup.splitDrag', function () { + if (isDragging) { + isDragging = false; + $divider.removeClass('dragging'); + $('body').removeClass('split-dragging'); + } + }); +}); diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/CreateVersionModal.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/CreateVersionModal.cshtml new file mode 100644 index 0000000000..38acee10e0 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/CreateVersionModal.cshtml @@ -0,0 +1,23 @@ +@page +@using Unity.AI.Localization +@using Unity.AI.Web.Pages.Prompts.Versions +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model CreateVersionModalModel +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/CreateVersionModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/CreateVersionModal.cshtml.cs new file mode 100644 index 0000000000..ba49f50d0a --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/CreateVersionModal.cshtml.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.AI.Prompts; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.AI.Web.Pages.Prompts.Versions; + +public class CreateVersionModalModel : AbpPageModel +{ + [BindProperty] + public CreateUpdateAIPromptVersionDto Version { get; set; } = new(); + + private readonly IAIPromptVersionAppService _versionAppService; + + public CreateVersionModalModel(IAIPromptVersionAppService versionAppService) + { + _versionAppService = versionAppService; + } + + public void OnGet(Guid promptId) + { + Version = new CreateUpdateAIPromptVersionDto + { + PromptId = promptId, + Temperature = 0.2 + }; + } + + public async Task OnPostAsync() + { + await _versionAppService.CreateAsync(Version); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/EditVersionModal.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/EditVersionModal.cshtml new file mode 100644 index 0000000000..2f1e96ea4f --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/EditVersionModal.cshtml @@ -0,0 +1,24 @@ +@page +@using Unity.AI.Localization +@using Unity.AI.Web.Pages.Prompts.Versions +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model EditVersionModalModel +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + + + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/EditVersionModal.cshtml.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/EditVersionModal.cshtml.cs new file mode 100644 index 0000000000..40390dd976 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Pages/Prompts/Versions/EditVersionModal.cshtml.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Unity.AI.Prompts; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace Unity.AI.Web.Pages.Prompts.Versions; + +public class EditVersionModalModel : AbpPageModel +{ + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public CreateUpdateAIPromptVersionDto Version { get; set; } = new(); + + private readonly IAIPromptVersionAppService _versionAppService; + + public EditVersionModalModel(IAIPromptVersionAppService versionAppService) + { + _versionAppService = versionAppService; + } + + public async Task OnGetAsync() + { + var dto = await _versionAppService.GetAsync(Id); + Version = new CreateUpdateAIPromptVersionDto + { + PromptId = dto.PromptId, + VersionNumber = dto.VersionNumber, + SystemPrompt = dto.SystemPrompt, + UserPromptTemplate = dto.UserPromptTemplate, + DeveloperNotes = dto.DeveloperNotes, + TargetModel = dto.TargetModel, + TargetProvider = dto.TargetProvider, + Temperature = dto.Temperature, + MaxTokens = dto.MaxTokens, + IsPublished = dto.IsPublished, + IsDeprecated = dto.IsDeprecated, + MetadataJson = dto.MetadataJson + }; + } + + public async Task OnPostAsync() + { + await _versionAppService.UpdateAsync(Id, Version); + return NoContent(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj index 79de5268e6..9b6f9cb856 100644 --- a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Unity.AI.Web.csproj @@ -8,6 +8,7 @@ true Library Unity.AI.Web + true @@ -24,6 +25,10 @@ + + + + diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIPromptsWidget/AIPromptsWidgetViewComponent.cs b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIPromptsWidget/AIPromptsWidgetViewComponent.cs new file mode 100644 index 0000000000..b397b8ea9a --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIPromptsWidget/AIPromptsWidgetViewComponent.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace Unity.AI.Web.Views.Shared.Components.AIPromptsWidget; + +[ViewComponent(Name = "AIPromptsWidget")] +public class AIPromptsWidgetViewComponent : AbpViewComponent +{ + public IViewComponentResult Invoke() + { + return View(); + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIPromptsWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIPromptsWidget/Default.cshtml new file mode 100644 index 0000000000..cdad35bf5e --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/Shared/Components/AIPromptsWidget/Default.cshtml @@ -0,0 +1,18 @@ +@using Unity.AI.Localization +@using Microsoft.Extensions.Localization +@inject IStringLocalizer L + +
+
+
+ +
+
+
+ + + + + +
+
diff --git a/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/_ViewImports.cshtml b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/_ViewImports.cshtml new file mode 100644 index 0000000000..231948b339 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.AI/src/Unity.AI.Web/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap +@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Permissions/FlexPermissions.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Permissions/FlexPermissions.cs similarity index 54% rename from applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Permissions/FlexPermissions.cs rename to applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Permissions/FlexPermissions.cs index 606d213668..efb26fabfe 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Permissions/FlexPermissions.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Permissions/FlexPermissions.cs @@ -1,4 +1,4 @@ -using Volo.Abp.Reflection; +using Volo.Abp.Reflection; namespace Unity.Flex.Permissions; @@ -6,6 +6,12 @@ public static class FlexPermissions { public const string GroupName = "Flex"; + public static class Worksheets + { + public const string Default = GroupName + ".Worksheets"; + public const string Delete = GroupName + ".Worksheets.Delete"; + } + public static string[] GetAll() { return ReflectionHelper.GetPublicConstantsRecursively(typeof(FlexPermissions)); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs index e36f44c45a..13f1168010 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/IWorksheetAppService.cs @@ -17,6 +17,7 @@ public interface IWorksheetAppService : IApplicationService Task CloneAsync(Guid id); Task PublishAsync(Guid id); Task DeleteAsync(Guid id); + Task GetLinkedFormsAsync(Guid worksheetId); Task ResequenceSectionsAsync(Guid id, uint oldIndex, uint newIndex); Task ExistsAsync(Guid worksheetId); Task ExportWorksheet(Guid worksheetId); diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetLinkedFormsDto.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetLinkedFormsDto.cs new file mode 100644 index 0000000000..59be5e4b47 --- /dev/null +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application.Contracts/Worksheets/WorksheetLinkedFormsDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Unity.Flex.Worksheets +{ + public class WorksheetLinkedFormsDto + { + public List FormVersionIdsWithInstances { get; set; } = []; + public List LinkedFormVersionIds { get; set; } = []; + } +} diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/WorksheetInstances/IWorksheetInstanceRepository.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/WorksheetInstances/IWorksheetInstanceRepository.cs index b60d3f98c7..4201df50ed 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/WorksheetInstances/IWorksheetInstanceRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Domain/WorksheetInstances/IWorksheetInstanceRepository.cs @@ -11,5 +11,6 @@ public interface IWorksheetInstanceRepository : IBasicRepository> GetByWorksheetCorrelationAsync(Guid worksheetId, string uiAnchor, Guid worksheetCorrelationId, string worksheetCorrelationProvider); Task GetWithValuesAsync(Guid worksheetInstanceId); Task ExistsAsync(Guid worksheetId, Guid instanceCorrelationId, string instanceCorrelationProvider, Guid sheetCorrelationId, string sheetCorrelationProvider, string? uiAnchor); + Task AnyByWorksheetAndFormVersionAsync(Guid worksheetId, Guid formVersionId); } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetInstanceRepository.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetInstanceRepository.cs index ee92456a11..71edcca8b6 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetInstanceRepository.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/EntityFrameworkCore/Repositories/WorksheetInstanceRepository.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Unity.Flex.Domain.WorksheetInstances; +using Unity.Modules.Shared.Correlation; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -50,11 +51,11 @@ public async Task> GetByWorksheetCorrelationAsync(Guid w .FirstOrDefaultAsync(wi => wi.Id == worksheetInstanceId); } - public async Task ExistsAsync(Guid worksheetId, - Guid instanceCorrelationId, - string instanceCorrelationProvider, - Guid sheetCorrelationId, - string sheetCorrelationProvider, + public async Task ExistsAsync(Guid worksheetId, + Guid instanceCorrelationId, + string instanceCorrelationProvider, + Guid sheetCorrelationId, + string sheetCorrelationProvider, string? uiAnchor) { var dbSet = await GetDbSetAsync(); @@ -67,5 +68,13 @@ public async Task ExistsAsync(Guid worksheetId, && s.WorksheetCorrelationProvider == sheetCorrelationProvider && s.UiAnchor == uiAnchor); } + + public async Task AnyByWorksheetAndFormVersionAsync(Guid worksheetId, Guid formVersionId) + { + var dbSet = await GetDbSetAsync(); + return await dbSet.AnyAsync(s => s.WorksheetId == worksheetId + && s.WorksheetCorrelationId == formVersionId + && s.WorksheetCorrelationProvider == CorrelationConsts.FormVersion); + } } } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Permissions/FlexPermissionDefinitionProvider.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Permissions/FlexPermissionDefinitionProvider.cs index 07d9f99706..f60eeac426 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Permissions/FlexPermissionDefinitionProvider.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Permissions/FlexPermissionDefinitionProvider.cs @@ -9,6 +9,19 @@ public class FlexPermissionDefinitionProvider : PermissionDefinitionProvider public override void Define(IPermissionDefinitionContext context) { context.AddGroup(FlexPermissions.GroupName, L("Permission:Flex")); + + var settingsMgmt = context.GetGroupOrNull("SettingManagement"); + if (settingsMgmt != null) + { + var configureWorksheet = settingsMgmt.AddPermission( + FlexPermissions.Worksheets.Default, + L("Permission:Flex.Worksheets") + ); + configureWorksheet.AddChild( + FlexPermissions.Worksheets.Delete, + L("Permission:Flex.Worksheets.Delete") + ); + } } private static LocalizableString L(string name) diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs index 47114d2e2e..90b41405a2 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Application/Worksheets/WorksheetAppService.cs @@ -7,8 +7,12 @@ using Unity.Flex.Domain.Services; using Unity.Flex.Domain.Settings; using Unity.Flex.Domain.Utils; +using Unity.Flex.Domain.WorksheetInstances; +using Unity.Flex.Domain.WorksheetLinks; using Unity.Flex.Domain.Worksheets; +using Unity.Flex.Permissions; using Unity.Flex.Reporting.FieldGenerators; +using Unity.Modules.Shared.Correlation; using Unity.Modules.Shared.Features; using Volo.Abp; using Volo.Abp.Features; @@ -19,7 +23,9 @@ namespace Unity.Flex.Worksheets public partial class WorksheetAppService(IWorksheetRepository worksheetRepository, WorksheetsManager worksheetsManager, IReportingFieldsGeneratorService reportingFieldsGeneratorService, - IFeatureChecker featureChecker) : FlexAppService, IWorksheetAppService + IFeatureChecker featureChecker, + IWorksheetLinkRepository worksheetLinkRepository, + IWorksheetInstanceRepository worksheetInstanceRepository) : FlexAppService, IWorksheetAppService { public virtual async Task GetAsync(Guid id) { @@ -118,11 +124,41 @@ public virtual async Task PublishAsync(Guid id) return await Task.FromResult(true); } + [Authorize(FlexPermissions.Worksheets.Delete)] public virtual async Task DeleteAsync(Guid id) { + var linkedForms = await GetLinkedFormsAsync(id); + + if (linkedForms.FormVersionIdsWithInstances.Count > 0) + { + throw new UserFriendlyException("This worksheet cannot be deleted because it has existing instances."); + } + + if (linkedForms.LinkedFormVersionIds.Count > 0) + { + throw new UserFriendlyException("This worksheet cannot be deleted because it is still linked to one or more forms. Unlink it first."); + } + await worksheetRepository.DeleteAsync(id); } + public virtual async Task GetLinkedFormsAsync(Guid worksheetId) + { + var links = await worksheetLinkRepository.GetListByWorksheetAsync(worksheetId, CorrelationConsts.FormVersion); + var result = new WorksheetLinkedFormsDto(); + + foreach (var correlationId in links.Select(link => link.CorrelationId)) + { + bool hasInstances = await worksheetInstanceRepository.AnyByWorksheetAndFormVersionAsync(worksheetId, correlationId); + if (hasInstances) + result.FormVersionIdsWithInstances.Add(correlationId); + else + result.LinkedFormVersionIds.Add(correlationId); + } + + return result; + } + public virtual async Task ResequenceSectionsAsync(Guid id, uint oldIndex, uint newIndex) { if (oldIndex == newIndex) return; diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json index 9f34865060..c615f56148 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Shared/Localization/Flex/en.json @@ -34,6 +34,8 @@ "Worksheet:Configuration:AddColumnOptionText": "Add Column", "DataGrids:DynamicColumnsHeader": "Dynamic Columns", "DataGrids:CustomColumnsHeader": "Custom Columns", - "DataGrids:PredefinedColumn": "Predefined column" + "DataGrids:PredefinedColumn": "Predefined column", + "Permission:Flex.Worksheets": "Configure Worksheet", + "Permission:Flex.Worksheets.Delete": "Delete Worksheet" } } \ No newline at end of file diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Default.cshtml b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Default.cshtml index 9a8fc69136..7455e7ae1e 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Default.cshtml +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Default.cshtml @@ -1,5 +1,6 @@ @using Microsoft.AspNetCore.Mvc.Localization @using Unity.Flex.Localization; +@using Unity.Flex.Permissions; @using Unity.Flex.Web.Views.Shared.Components.Worksheets; @using Volo.Abp.Authorization.Permissions; @@ -31,6 +32,12 @@ + @if (await PermissionChecker.IsGrantedAsync(FlexPermissions.Worksheets.Delete)) + { + + } diff --git a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js index 0177fdd53a..90f290ecbf 100644 --- a/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js +++ b/applications/Unity.GrantManager/modules/Unity.Flex/src/Unity.Flex.Web/Views/Shared/Components/WorksheetWidget/Worksheet.js @@ -79,6 +79,15 @@ $(function () { }); } + let deleteWorksheetButtons = $(".delete-worksheet-btn"); + + if (deleteWorksheetButtons) { + deleteWorksheetButtons.on("click", function (event) { + let btn = event.currentTarget; + handleDeleteWorksheet(btn.dataset.worksheetId, btn.dataset.worksheetTitle, btn.dataset.worksheetName); + }); + } + setupTooltips(); } @@ -223,3 +232,46 @@ $(function () { } ); }); + +function handleDeleteWorksheet(worksheetId, worksheetTitle, worksheetName) { + unity.grantManager.settingManagement.worksheetConfiguration.getDeletionCheck(worksheetId) + .done(function (result) { + if (result.blockingFormNames && result.blockingFormNames.length > 0) { + abp.message.error( + 'This worksheet cannot be deleted because it is already used by the following forms:\n' + result.blockingFormNames.join('\n'), + 'Delete Worksheet' + ); + } else if (result.linkedFormNames && result.linkedFormNames.length > 0) { + abp.message.error( + 'Unlink the worksheet (' + worksheetTitle + ' \u2013 ' + worksheetName + ') from the following forms before deletion:\n' + result.linkedFormNames.join('\n'), + 'Delete Worksheet' + ); + } else { + abp.message.confirm( + 'Are you sure you want to delete the worksheet "' + worksheetTitle + '"?', + 'Delete Worksheet', + function (confirmed) { + if (confirmed) { + executeWorksheetDelete(worksheetId); + } + } + ); + } + }) + .fail(function (e) { + abp.notify.error('Failed to check worksheet deletion status.'); + console.warn('Worksheet deletion check failed:', e); + }); +} + +function executeWorksheetDelete(worksheetId) { + unity.flex.worksheets.worksheet.delete(worksheetId) + .done(function () { + PubSub.publish('refresh_worksheet_list', { worksheetId: worksheetId, action: 'Delete' }); + abp.notify.success('Worksheet deleted successfully.', 'Delete Worksheet'); + }) + .fail(function (e) { + abp.notify.error('Failed to delete worksheet.'); + console.warn('Worksheet deletion failed:', e); + }); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs new file mode 100644 index 0000000000..7829f8028b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateApplicationAnalysisBackgroundJobArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Unity.GrantManager.AI.BackgroundJobs; + +public class GenerateApplicationAnalysisBackgroundJobArgs +{ + public Guid ApplicationId { get; set; } + public string? PromptVersion { get; set; } + public Guid? TenantId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs new file mode 100644 index 0000000000..234f8ec706 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateApplicationScoringBackgroundJobArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Unity.GrantManager.AI.BackgroundJobs; + +public class GenerateApplicationScoringBackgroundJobArgs +{ + public Guid ApplicationId { get; set; } + public string? PromptVersion { get; set; } + public Guid? TenantId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs new file mode 100644 index 0000000000..7836e5abe3 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateAttachmentSummaryBackgroundJobArgs.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Unity.GrantManager.AI.BackgroundJobs; + +public class GenerateAttachmentSummaryBackgroundJobArgs +{ + public List AttachmentIds { get; set; } = []; + public string? PromptVersion { get; set; } + public Guid? TenantId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateContentBackgroundJobArgs.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateContentBackgroundJobArgs.cs new file mode 100644 index 0000000000..d320bf8316 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/BackgroundJobs/GenerateContentBackgroundJobArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Unity.GrantManager.AI.BackgroundJobs; + +public class GenerateContentBackgroundJobArgs +{ + public Guid ApplicationId { get; set; } + public string? PromptVersion { get; set; } + public Guid? TenantId { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ITextExtractionService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Extraction/ITextExtractionService.cs similarity index 81% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ITextExtractionService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Extraction/ITextExtractionService.cs index 22f34e292f..1e68280b99 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/ITextExtractionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Extraction/ITextExtractionService.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Extraction { public interface ITextExtractionService { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs index d14438a2d2..d059187316 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/IAIService.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using Unity.GrantManager.AI.Requests; +using Unity.GrantManager.AI.Responses; namespace Unity.GrantManager.AI { @@ -9,6 +11,6 @@ public interface IAIService Task GenerateCompletionAsync(AICompletionRequest request); Task GenerateAttachmentSummaryAsync(AttachmentSummaryRequest request); Task GenerateApplicationAnalysisAsync(ApplicationAnalysisRequest request); - Task GenerateScoresheetSectionAsync(ScoresheetSectionRequest request); + Task GenerateApplicationScoringAsync(ApplicationScoringRequest request); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIAttachmentItem.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIAttachmentItem.cs index fc4b31e2a9..58c7c1c65e 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIAttachmentItem.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIAttachmentItem.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Models { public class AIAttachmentItem { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIJsonKeys.cs similarity index 95% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIJsonKeys.cs index 60a5b52441..fb2230cba5 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/AIJsonKeys.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/AIJsonKeys.cs @@ -1,4 +1,4 @@ -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Models { public static class AIJsonKeys { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs index d441d29493..47baf9bca1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisFinding.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Models { public class ApplicationAnalysisFinding { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs index 1a70d1a5f4..c84a3d4793 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationAnalysisRecommendation.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Models { public class ApplicationAnalysisRecommendation { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ScoresheetSectionAnswer.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationScoringAnswer.cs similarity index 82% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ScoresheetSectionAnswer.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationScoringAnswer.cs index 0a76cbb0e0..7796460557 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ScoresheetSectionAnswer.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Models/ApplicationScoringAnswer.cs @@ -1,9 +1,9 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Models { - public class ScoresheetSectionAnswer + public class ApplicationScoringAnswer { [JsonPropertyName(AIJsonKeys.Answer)] public JsonElement Answer { get; set; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs index 74b8aa5494..56dbc6fc4d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AICompletionRequest.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Requests { public class AICompletionRequest { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs index 3d9aaf789f..5366b29877 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationAnalysisRequest.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +using Unity.GrantManager.AI.Models; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Requests { public class ApplicationAnalysisRequest { @@ -17,11 +18,5 @@ public class ApplicationAnalysisRequest [JsonPropertyName("promptVersion")] public string? PromptVersion { get; set; } - - [JsonPropertyName("capturePromptIo")] - public bool CapturePromptIo { get; set; } - - [JsonPropertyName("captureContextId")] - public string? CaptureContextId { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ScoresheetSectionRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationScoringRequest.cs similarity index 69% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ScoresheetSectionRequest.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationScoringRequest.cs index 7f904ea77a..241a46a977 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ScoresheetSectionRequest.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/ApplicationScoringRequest.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +using Unity.GrantManager.AI.Models; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Requests { - public class ScoresheetSectionRequest + public class ApplicationScoringRequest { [JsonPropertyName("data")] public JsonElement Data { get; set; } @@ -20,11 +21,5 @@ public class ScoresheetSectionRequest [JsonPropertyName("promptVersion")] public string? PromptVersion { get; set; } - - [JsonPropertyName("capturePromptIo")] - public bool CapturePromptIo { get; set; } - - [JsonPropertyName("captureContextId")] - public string? CaptureContextId { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs index d3eb7fe217..4f4d009e86 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Requests/AttachmentSummaryRequest.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Requests { public class AttachmentSummaryRequest { @@ -15,11 +15,5 @@ public class AttachmentSummaryRequest [JsonPropertyName("promptVersion")] public string? PromptVersion { get; set; } - - [JsonPropertyName("capturePromptIo")] - public bool CapturePromptIo { get; set; } - - [JsonPropertyName("captureContextId")] - public string? CaptureContextId { get; set; } } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs index 316d2ef162..a146c7f86a 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AICompletionResponse.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Responses { public class AICompletionResponse { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AIPromptCaptureResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AIPromptCaptureResponse.cs deleted file mode 100644 index 5c60ea2ae0..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AIPromptCaptureResponse.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace Unity.GrantManager.AI -{ - public class AIPromptCaptureResponse - { - [JsonPropertyName("contextId")] - public string ContextId { get; set; } = string.Empty; - - [JsonPropertyName("promptType")] - public string PromptType { get; set; } = string.Empty; - - [JsonPropertyName("promptVersion")] - public string PromptVersion { get; set; } = string.Empty; - - [JsonPropertyName("captureLabel")] - public string CaptureLabel { get; set; } = string.Empty; - - [JsonPropertyName("systemPrompt")] - public string SystemPrompt { get; set; } = string.Empty; - - [JsonPropertyName("userPrompt")] - public string UserPrompt { get; set; } = string.Empty; - - [JsonPropertyName("output")] - public string Output { get; set; } = string.Empty; - - [JsonPropertyName("capturedAt")] - public DateTime CapturedAt { get; set; } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs index 705b713c00..fda17e43d6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationAnalysisResponse.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using System.Text.Json.Serialization; +using Unity.GrantManager.AI.Models; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Responses { public class ApplicationAnalysisResponse { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationScoringResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationScoringResponse.cs new file mode 100644 index 0000000000..3a5ce46501 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ApplicationScoringResponse.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Unity.GrantManager.AI.Models; + +namespace Unity.GrantManager.AI.Responses +{ + public class ApplicationScoringResponse + { + [JsonPropertyName("answers")] + public Dictionary Answers { get; set; } = new(); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs index 4f30b8c44a..170345cad1 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/AttachmentSummaryResponse.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; +using Unity.GrantManager.AI.Models; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Responses { public class AttachmentSummaryResponse { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ScoresheetSectionResponse.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ScoresheetSectionResponse.cs deleted file mode 100644 index cf4569dd07..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/AI/Responses/ScoresheetSectionResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Unity.GrantManager.AI -{ - public class ScoresheetSectionResponse - { - [JsonPropertyName("answers")] - public Dictionary Answers { get; set; } = new(); - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/ApplicationChefsFileAttachmentDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/ApplicationChefsFileAttachmentDto.cs index 9e9c9c3b12..fd2555acec 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/ApplicationChefsFileAttachmentDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/ApplicationChefsFileAttachmentDto.cs @@ -3,10 +3,13 @@ namespace Unity.GrantManager.Attachments; -public class ApplicationChefsFileAttachmentDto : EntityDto -{ - public Guid ApplicationId { get; set; } - public string ChefsSubmissionId { get; set; } = string.Empty; - public string ChefsFileId { get; set; } = string.Empty; - public string? Name { get; set; } -} +public class ApplicationChefsFileAttachmentDto : EntityDto +{ + public Guid ApplicationId { get; set; } + public string ChefsSubmissionId { get; set; } = string.Empty; + public string ChefsFileId { get; set; } = string.Empty; + public string? Name { get; set; } + public string? AISummary { get; set; } + public DateTime CreatedTime { get; set; } + public DateTime? UpdatedTime { get; set; } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/IAttachmentAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/IAttachmentAppService.cs index b6bb290746..1b638f3bc3 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/IAttachmentAppService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/IAttachmentAppService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System; using System.Threading.Tasks; using Volo.Abp.Application.Services; @@ -6,13 +6,11 @@ namespace Unity.GrantManager.Attachments; public interface IAttachmentAppService : IApplicationService -{ +{ Task> GetApplicationAsync(Guid applicationId); Task> GetAssessmentAsync(Guid assessmentId); Task ResyncSubmissionAttachmentsAsync(Guid applicationId); Task> GetAttachmentsAsync(AttachmentParametersDto attachmentParametersDto); Task GetAttachmentMetadataAsync(AttachmentType attachmentType, Guid attachmentId); Task UpdateAttachmentMetadataAsync(UpdateAttachmentMetadataDto updateAttachment); - Task GenerateAISummaryAttachmentAsync(Guid attachmentId, string? promptVersion = null, bool capturePromptIo = false); - Task> GenerateAISummariesAttachmentsAsync(List attachmentIds, string? promptVersion = null, bool capturePromptIo = false); } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs new file mode 100644 index 0000000000..65589ed840 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/Attachments/IAttachmentSummaryAppService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.Attachments; + +public interface IAttachmentSummaryAppService : IApplicationService +{ + Task GenerateAttachmentSummaryAsync(Guid attachmentId, string? promptVersion = null); + Task> GenerateAttachmentSummariesAsync(List attachmentIds, string? promptVersion = null); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs index f2906d1166..1dda8c6696 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/GrantApplicationDto.cs @@ -1,6 +1,6 @@ -using System; +using System; using System.Collections.Generic; -using Unity.GrantManager.AI; +using Unity.GrantManager.AI.Responses; using Unity.GrantManager.ApplicationForms; using Volo.Abp.Application.Dtos; @@ -85,4 +85,5 @@ public class GrantApplicationDto : AuditedEntityDto public string? ApplicantElectoralDistrict { get; set; } public string? AIAnalysis { get; set; } public ApplicationAnalysisResponse? AIAnalysisData { get; set; } + public string? AIScoresheetAnswers { get; set; } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAIAnalysisAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAIAnalysisAppService.cs deleted file mode 100644 index cfb21ea57c..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAIAnalysisAppService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.GrantApplications -{ - public interface IApplicationAIAnalysisAppService : IApplicationService - { - Task GenerateAIAnalysisAsync(Guid applicationId, string? promptVersion = null, bool capturePromptIo = false); - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAIPromptCaptureAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAIPromptCaptureAppService.cs deleted file mode 100644 index c25d04ee9b..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAIPromptCaptureAppService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using System; -using System.Threading.Tasks; -using Unity.GrantManager.AI; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.GrantApplications -{ - public interface IApplicationAIPromptCaptureAppService : IApplicationService - { - Task> GetRecentAsync(Guid applicationId, string promptType, string? promptVersion = null); - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAIScoringAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAIScoringAppService.cs deleted file mode 100644 index e3f54c8f2e..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAIScoringAppService.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Threading.Tasks; -using Volo.Abp.Application.Services; - -namespace Unity.GrantManager.GrantApplications -{ - public interface IApplicationAIScoringAppService : IApplicationService - { - Task GenerateAIScoresheetAnswersAsync(Guid applicationId, string? promptVersion = null, bool capturePromptIo = false); - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs new file mode 100644 index 0000000000..493eb82c56 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationAnalysisAppService.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.GrantApplications +{ + public interface IApplicationAnalysisAppService : IApplicationService + { + Task GenerateApplicationAnalysisAsync(Guid applicationId, string? promptVersion = null); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationContentAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationContentAppService.cs new file mode 100644 index 0000000000..c2d27129a6 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationContentAppService.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.GrantApplications; + +public interface IApplicationContentAppService : IApplicationService +{ + Task GenerateContentAsync(Guid applicationId, string? promptVersion = null); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs new file mode 100644 index 0000000000..dae0bc4fb7 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/GrantApplications/IApplicationScoringAppService.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.GrantApplications +{ + public interface IApplicationScoringAppService : IApplicationService + { + Task GenerateApplicationScoringAsync(Guid applicationId, string? promptVersion = null); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/IWorksheetConfigurationAppService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/IWorksheetConfigurationAppService.cs new file mode 100644 index 0000000000..05b0159aec --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/IWorksheetConfigurationAppService.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Unity.GrantManager.SettingManagement; + +public interface IWorksheetConfigurationAppService : IApplicationService +{ + Task GetDeletionCheckAsync(Guid worksheetId); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/WorksheetDeletionCheckDto.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/WorksheetDeletionCheckDto.cs new file mode 100644 index 0000000000..0ff346c41b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application.Contracts/SettingManagement/WorksheetDeletionCheckDto.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Unity.GrantManager.SettingManagement; + +public class WorksheetDeletionCheckDto +{ + public List BlockingFormNames { get; set; } = []; + public List LinkedFormNames { get; set; } = []; +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/AIPromptCaptureStore.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/AIPromptCaptureStore.cs deleted file mode 100644 index ec69a9de20..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/AIPromptCaptureStore.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using Volo.Abp.DependencyInjection; - -namespace Unity.GrantManager.AI -{ - public class AIPromptCaptureStore : IAIPromptCaptureStore, ISingletonDependency - { - private const int MaxCapturesPerKey = 50; - private readonly ConcurrentDictionary> _captures = new(StringComparer.OrdinalIgnoreCase); - - public void Save(AIPromptCaptureResponse capture) - { - var key = BuildKey(capture.ContextId, capture.PromptType, capture.PromptVersion); - var queue = _captures.GetOrAdd(key, _ => new ConcurrentQueue()); - queue.Enqueue(capture); - - while (queue.Count > MaxCapturesPerKey) - { - queue.TryDequeue(out _); - } - } - - public IReadOnlyList GetRecent(string contextId, string promptType, string? promptVersion = null, int maxResults = 20) - { - if (!string.IsNullOrWhiteSpace(promptVersion)) - { - var key = BuildKey(contextId, promptType, promptVersion); - return _captures.TryGetValue(key, out var captures) - ? captures.OrderByDescending(item => item.CapturedAt).Take(maxResults).ToList() - : Array.Empty(); - } - - return _captures.Values - .SelectMany(queue => queue) - .Where(item => string.Equals(item.ContextId, contextId, StringComparison.OrdinalIgnoreCase) - && string.Equals(item.PromptType, promptType, StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(item => item.CapturedAt) - .Take(maxResults) - .ToList(); - } - - private static string BuildKey(string contextId, string promptType, string promptVersion) - { - return $"{contextId.Trim()}::{promptType.Trim()}::{promptVersion.Trim()}"; - } - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateApplicationAnalysisBackgroundJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateApplicationAnalysisBackgroundJob.cs new file mode 100644 index 0000000000..e9bd6ee84b --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateApplicationAnalysisBackgroundJob.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using Unity.GrantManager.AI.Operations; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.AI.BackgroundJobs; + +public class GenerateApplicationAnalysisBackgroundJob( + IApplicationAnalysisService applicationAnalysisService, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateApplicationAnalysisBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + logger.LogInformation("Executing AI application analysis background job for application {ApplicationId}.", args.ApplicationId); + await applicationAnalysisService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateApplicationScoringBackgroundJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateApplicationScoringBackgroundJob.cs new file mode 100644 index 0000000000..a445993a68 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateApplicationScoringBackgroundJob.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.AI.Operations; +using Unity.GrantManager.Intakes.Events; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Local; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.AI.BackgroundJobs; + +public class GenerateApplicationScoringBackgroundJob( + IApplicationScoringService applicationScoringService, + ILocalEventBus localEventBus, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateApplicationScoringBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + logger.LogInformation("Executing AI application scoring background job for application {ApplicationId}.", args.ApplicationId); + + var result = await applicationScoringService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); + if (!string.Equals(result, "{}", StringComparison.Ordinal)) + { + await localEventBus.PublishAsync(new AIApplicationScoringGeneratedEvent + { + ApplicationId = args.ApplicationId + }); + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateAttachmentSummaryBackgroundJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateAttachmentSummaryBackgroundJob.cs new file mode 100644 index 0000000000..ed6a1ebd91 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateAttachmentSummaryBackgroundJob.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using Unity.GrantManager.AI.Operations; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.AI.BackgroundJobs; + +public class GenerateAttachmentSummaryBackgroundJob( + IAttachmentSummaryService attachmentSummaryService, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateAttachmentSummaryBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + logger.LogInformation( + "Executing AI attachment summary background job for {AttachmentCount} attachment(s).", + args.AttachmentIds.Count); + + await attachmentSummaryService.GenerateAndSaveAsync(args.AttachmentIds, args.PromptVersion); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateContentBackgroundJob.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateContentBackgroundJob.cs new file mode 100644 index 0000000000..7a3fe34a40 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/BackgroundJobs/GenerateContentBackgroundJob.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; +using Unity.GrantManager.AI.Operations; +using Unity.GrantManager.Intakes.Events; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Local; +using Volo.Abp.Features; +using Volo.Abp.MultiTenancy; + +namespace Unity.GrantManager.AI.BackgroundJobs; + +public class GenerateContentBackgroundJob( + IAttachmentSummaryService attachmentSummaryService, + IApplicationAnalysisService applicationAnalysisService, + IApplicationScoringService applicationScoringService, + IAIService aiService, + IFeatureChecker featureChecker, + ILocalEventBus localEventBus, + ICurrentTenant currentTenant, + ILogger logger) : AsyncBackgroundJob, ITransientDependency +{ + public override async Task ExecuteAsync(GenerateContentBackgroundJobArgs args) + { + using (currentTenant.Change(args.TenantId)) + { + var attachmentSummariesEnabled = await featureChecker.IsEnabledAsync("Unity.AI.AttachmentSummaries"); + var applicationAnalysisEnabled = await featureChecker.IsEnabledAsync("Unity.AI.ApplicationAnalysis"); + var scoringEnabled = await featureChecker.IsEnabledAsync("Unity.AI.Scoring"); + + if (!attachmentSummariesEnabled && !applicationAnalysisEnabled && !scoringEnabled) + { + logger.LogDebug("All AI features are disabled, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId); + return; + } + + if (!await aiService.IsAvailableAsync()) + { + logger.LogWarning("AI service is not available, skipping queued AI generation for application {ApplicationId}.", args.ApplicationId); + return; + } + + logger.LogInformation("Executing queued AI content pipeline for application {ApplicationId}.", args.ApplicationId); + + if (attachmentSummariesEnabled) + { + await attachmentSummaryService.GenerateForApplicationAsync(args.ApplicationId, args.PromptVersion); + } + + Exception? analysisException = null; + Exception? scoringException = null; + + if (applicationAnalysisEnabled) + { + try + { + await applicationAnalysisService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); + } + catch (Exception ex) + { + analysisException = ex; + logger.LogError(ex, "Error executing AI application analysis stage for application {ApplicationId}.", args.ApplicationId); + } + } + + if (scoringEnabled) + { + try + { + var result = await applicationScoringService.RegenerateAndSaveAsync(args.ApplicationId, args.PromptVersion); + if (!string.Equals(result, "{}", StringComparison.Ordinal)) + { + await localEventBus.PublishAsync(new AIApplicationScoringGeneratedEvent + { + ApplicationId = args.ApplicationId + }); + } + } + catch (Exception ex) + { + scoringException = ex; + logger.LogError(ex, "Error executing AI application scoring stage for application {ApplicationId}.", args.ApplicationId); + } + } + + if (scoringException != null) + { + throw scoringException; + } + + if (analysisException != null) + { + throw analysisException; + } + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Extraction/TextExtractionService.cs similarity index 99% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Extraction/TextExtractionService.cs index c45eeb9d36..a29d4cad30 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/TextExtractionService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Extraction/TextExtractionService.cs @@ -13,7 +13,7 @@ using UglyToad.PdfPig; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Extraction { public partial class TextExtractionService : ITextExtractionService, ITransientDependency { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IAIPromptCaptureStore.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IAIPromptCaptureStore.cs deleted file mode 100644 index 7c2e5d301a..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IAIPromptCaptureStore.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace Unity.GrantManager.AI -{ - public interface IAIPromptCaptureStore - { - void Save(AIPromptCaptureResponse capture); - IReadOnlyList GetRecent(string contextId, string promptType, string? promptVersion = null, int maxResults = 20); - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IApplicationScoresheetAnalysisService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IApplicationScoresheetAnalysisService.cs deleted file mode 100644 index 1cc4ef1ffa..0000000000 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IApplicationScoresheetAnalysisService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Unity.GrantManager.AI -{ - public interface IApplicationScoresheetAnalysisService - { - Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, bool capturePromptIo = false); - } -} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationAnalysisService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/ApplicationAnalysisService.cs similarity index 97% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationAnalysisService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/ApplicationAnalysisService.cs index 4b633cfd82..a06863fc8d 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/ApplicationAnalysisService.cs @@ -5,10 +5,13 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using Unity.GrantManager.AI.Models; +using Unity.GrantManager.AI.Prompts; +using Unity.GrantManager.AI.Requests; using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Operations { public class ApplicationAnalysisService( IApplicationRepository applicationRepository, @@ -29,7 +32,7 @@ public class ApplicationAnalysisService( "applicantAgent" }; - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, bool capturePromptIo = false) + public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null) { var application = await applicationRepository.GetAsync(applicationId); var formSubmission = await applicationFormSubmissionRepository.GetByApplicationAsync(applicationId); @@ -57,8 +60,6 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro Data = PromptDataPayloadBuilder.BuildPromptDataPayload(application, formSubmission, formSchema, logger), Attachments = attachmentSummaries, PromptVersion = promptVersion, - CapturePromptIo = capturePromptIo, - CaptureContextId = applicationId.ToString() }); var analysisJson = JsonSerializer.Serialize(analysis, _jsonOptionsIndented); diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationScoresheetAnalysisService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/ApplicationScoringService.cs similarity index 86% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationScoresheetAnalysisService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/ApplicationScoringService.cs index 82b7c12ae4..b73b76fe27 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/ApplicationScoresheetAnalysisService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/ApplicationScoringService.cs @@ -5,12 +5,15 @@ using System.Text.Json; using System.Threading.Tasks; using Unity.Flex.Domain.Scoresheets; +using Unity.GrantManager.AI.Models; +using Unity.GrantManager.AI.Prompts; +using Unity.GrantManager.AI.Requests; using Unity.GrantManager.Applications; using Volo.Abp.DependencyInjection; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Operations { - public class ApplicationScoresheetAnalysisService( + public class ApplicationScoringService( IApplicationRepository applicationRepository, IApplicationFormRepository applicationFormRepository, IApplicationFormSubmissionRepository applicationFormSubmissionRepository, @@ -18,7 +21,7 @@ public class ApplicationScoresheetAnalysisService( IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, IScoresheetRepository scoresheetRepository, IAIService aiService, - ILogger logger) : IApplicationScoresheetAnalysisService, ITransientDependency + ILogger logger) : IApplicationScoringService, ITransientDependency { private readonly JsonSerializerOptions _jsonOptions = new() { @@ -31,7 +34,7 @@ public class ApplicationScoresheetAnalysisService( WriteIndented = true }; - public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, bool capturePromptIo = false) + public async Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null) { var application = await applicationRepository.GetAsync(applicationId); var applicationForm = await applicationFormRepository.GetAsync(application.ApplicationFormId); @@ -80,21 +83,19 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro }); } - var sectionRequest = new ScoresheetSectionRequest + var applicationScoringRequest = new ApplicationScoringRequest { Data = promptData, Attachments = attachmentSummaries, SectionName = section.Name, SectionSchema = JsonSerializer.SerializeToElement(sectionQuestionsData, _jsonOptions), PromptVersion = promptVersion, - CapturePromptIo = capturePromptIo, - CaptureContextId = applicationId.ToString() }; - var sectionAnswers = await aiService.GenerateScoresheetSectionAsync(sectionRequest); + var applicationScoringResponse = await aiService.GenerateApplicationScoringAsync(applicationScoringRequest); - if (sectionAnswers.Answers.Count > 0) + if (applicationScoringResponse.Answers.Count > 0) { - var sectionJson = JsonSerializer.Serialize(sectionAnswers.Answers, _jsonOptions); + var sectionJson = JsonSerializer.Serialize(applicationScoringResponse.Answers, _jsonOptions); using var sectionDoc = JsonDocument.Parse(sectionJson); foreach (var property in sectionDoc.RootElement.EnumerateObject()) { @@ -104,12 +105,12 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro } catch (Exception ex) { - logger.LogError(ex, "Error processing AI scoresheet section {SectionName} for application {ApplicationId}", section.Name, applicationId); + logger.LogError(ex, "Error processing AI application scoring section {SectionName} for application {ApplicationId}", section.Name, applicationId); } } var combinedResults = JsonSerializer.Serialize(allSectionResults, _jsonOptionsIndented); - var validatedJson = ValidateScoresheetJson(combinedResults); + var validatedJson = ValidateApplicationScoringJson(combinedResults); application.AIScoresheetAnswers = validatedJson; await applicationRepository.UpdateAsync(application); return validatedJson; @@ -129,12 +130,12 @@ public async Task RegenerateAndSaveAsync(Guid applicationId, string? pro } catch (Exception ex) { - logger.LogWarning(ex, "Unable to load form schema for scoresheet prompt data generation for form version {FormVersionId}.", formVersionId); + logger.LogWarning(ex, "Unable to load form schema for application scoring prompt data generation for form version {FormVersionId}.", formVersionId); return null; } } - private static string ValidateScoresheetJson(string scoresheetAnswers) + private static string ValidateApplicationScoringJson(string scoresheetAnswers) { try { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/AttachmentSummaryService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/AttachmentSummaryService.cs new file mode 100644 index 0000000000..eb8f84be83 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/AttachmentSummaryService.cs @@ -0,0 +1,104 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Unity.GrantManager.AI.Requests; +using Unity.GrantManager.Applications; +using Unity.GrantManager.Intakes; +using Volo.Abp.DependencyInjection; + +namespace Unity.GrantManager.AI.Operations; + +public class AttachmentSummaryService( + IApplicationChefsFileAttachmentRepository applicationChefsFileAttachmentRepository, + ISubmissionAppService submissionAppService, + IAIService aiService, + ILogger logger) : IAttachmentSummaryService, ITransientDependency +{ + private const string DefaultContentType = "application/octet-stream"; + private const string SummaryGenerationFailedMessage = "AI summary generation failed."; + + public async Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null) + { + var attachment = await applicationChefsFileAttachmentRepository.GetAsync(attachmentId); + var fileName = string.IsNullOrWhiteSpace(attachment.FileName) ? "unknown" : attachment.FileName; + var (fileContent, contentType) = await GetAttachmentContentForSummaryAsync(attachment, fileName); + + var summaryResponse = await aiService.GenerateAttachmentSummaryAsync(new AttachmentSummaryRequest + { + FileName = fileName, + FileContent = fileContent, + ContentType = contentType, + PromptVersion = promptVersion, + }); + + attachment.AISummary = summaryResponse.Summary; + await applicationChefsFileAttachmentRepository.UpdateAsync(attachment); + + return summaryResponse.Summary; + } + + public async Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null) + { + var summaries = new List(); + + foreach (var attachmentId in attachmentIds) + { + try + { + summaries.Add(await GenerateAndSaveAsync(attachmentId, promptVersion)); + } + catch (Exception ex) + { + logger.LogError(ex, "Error generating AI summary for attachment {AttachmentId}", attachmentId); + summaries.Add(SummaryGenerationFailedMessage); + } + } + + return summaries; + } + + public async Task> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null) + { + var attachmentIds = (await applicationChefsFileAttachmentRepository.GetListAsync(a => a.ApplicationId == applicationId)) + .Select(a => a.Id) + .ToList(); + + return await GenerateAndSaveAsync(attachmentIds, promptVersion); + } + + private async Task<(byte[] Content, string ContentType)> GetAttachmentContentForSummaryAsync(ApplicationChefsFileAttachment attachment, string fileName) + { + if (!Guid.TryParse(attachment.ChefsSubmissionId, out var submissionId) || + !Guid.TryParse(attachment.ChefsFileId, out var fileId)) + { + logger.LogWarning( + "Attachment {AttachmentId} has invalid CHEFS IDs. Falling back to metadata-only summary generation.", + attachment.Id); + return (Array.Empty(), DefaultContentType); + } + + try + { + var fileDto = await submissionAppService.GetChefsFileAttachment(submissionId, fileId, fileName); + if (fileDto?.Content == null) + { + logger.LogWarning( + "Attachment {AttachmentId} has no retrievable content. Falling back to metadata-only summary generation.", + attachment.Id); + return (Array.Empty(), DefaultContentType); + } + + return (fileDto.Content, string.IsNullOrWhiteSpace(fileDto.ContentType) ? DefaultContentType : fileDto.ContentType); + } + catch (Exception ex) + { + logger.LogWarning( + ex, + "Failed retrieving CHEFS content for attachment {AttachmentId}. Falling back to metadata-only summary generation.", + attachment.Id); + return (Array.Empty(), DefaultContentType); + } + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IApplicationAnalysisService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/IApplicationAnalysisService.cs similarity index 65% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IApplicationAnalysisService.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/IApplicationAnalysisService.cs index 172a3b9c5a..38eb57b046 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/IApplicationAnalysisService.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/IApplicationAnalysisService.cs @@ -1,10 +1,10 @@ using System; using System.Threading.Tasks; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Operations { public interface IApplicationAnalysisService { - Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null, bool capturePromptIo = false); + Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null); } } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/IApplicationScoringService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/IApplicationScoringService.cs new file mode 100644 index 0000000000..ff76afb866 --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/IApplicationScoringService.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Unity.GrantManager.AI.Operations +{ + public interface IApplicationScoringService + { + Task RegenerateAndSaveAsync(Guid applicationId, string? promptVersion = null); + } +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/IAttachmentSummaryService.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/IAttachmentSummaryService.cs new file mode 100644 index 0000000000..a4cdfd8f9f --- /dev/null +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Operations/IAttachmentSummaryService.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Unity.GrantManager.AI.Operations; + +public interface IAttachmentSummaryService +{ + Task GenerateAndSaveAsync(Guid attachmentId, string? promptVersion = null); + Task> GenerateAndSaveAsync(IEnumerable attachmentIds, string? promptVersion = null); + Task> GenerateForApplicationAsync(Guid applicationId, string? promptVersion = null); +} diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/AIPromptTypes.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AIPromptTypes.cs similarity index 61% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/AIPromptTypes.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AIPromptTypes.cs index 41ce17e33e..47870da3f6 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/AIPromptTypes.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/AIPromptTypes.cs @@ -1,8 +1,8 @@ -namespace Unity.GrantManager.AI; +namespace Unity.GrantManager.AI.Prompts; public static class AIPromptTypes { public const string AttachmentSummary = "AttachmentSummary"; public const string ApplicationAnalysis = "ApplicationAnalysis"; - public const string ScoresheetSection = "ScoresheetSection"; + public const string ApplicationScoring = "ApplicationScoring"; } diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/PromptDataPayloadBuilder.cs b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/PromptDataPayloadBuilder.cs similarity index 99% rename from applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/PromptDataPayloadBuilder.cs rename to applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/PromptDataPayloadBuilder.cs index ec60077961..c24166e354 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/PromptDataPayloadBuilder.cs +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/PromptDataPayloadBuilder.cs @@ -6,7 +6,7 @@ using System.Text.Json; using Unity.GrantManager.Applications; -namespace Unity.GrantManager.AI +namespace Unity.GrantManager.AI.Prompts { internal static class PromptDataPayloadBuilder { diff --git a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Versions/README.md b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Versions/README.md index 0a2ae41b7b..3cc30965e8 100644 --- a/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Versions/README.md +++ b/applications/Unity.GrantManager/src/Unity.GrantManager.Application/AI/Prompts/Versions/README.md @@ -1,27 +1,27 @@ # Runtime Prompt Templates These files are the source of truth for runtime prompts. -`OpenAIService` resolves templates from: +`OpenAIRuntimeService` resolves templates from: - `AI/Prompts/Versions//