Conversation
Summary of ChangesHello @ElasticBottle, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request enhances internal tooling by providing a dedicated admin interface and API endpoints to trigger AI-driven workflows for project onboarding and strategy generation. It also refactors the underlying schema conversion logic for AI models, improving the robustness and compatibility of schema definitions across various AI-powered features. These changes streamline the management and execution of key AI processes within the application. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
commit: |
There was a problem hiding this comment.
Code Review
This pull request introduces a new admin page for triggering workflows and refactors how ArkType schemas are converted to JSON schemas for AI tool use. The refactoring to a centralized arktypeToAiJsonSchema utility is a good improvement for consistency and maintainability. However, I've identified a couple of issues. The new admin page has a bug where form inputs share state, which could lead to incorrect workflow triggers. Additionally, there's a potential issue in the new schema transformation logic that could generate invalid JSON schemas for certain union types. My review includes suggestions to fix these problems.
There was a problem hiding this comment.
Pull request overview
This PR fixes workflow generation issues by introducing proper ArkType-to-AI-JSON-Schema transformation and adds admin endpoints for manual workflow triggering. The core issue being addressed is that ArkType schemas weren't being correctly converted to JSON Schema format compatible with the AI SDK's structured output requirements.
Changes:
- Introduced
arktypeToAiJsonSchemahelper to properly transform ArkType schemas to AI SDK compatible JSON schemas - Changed
descriptionfield instrategySuggestionSchemafrom optional (description?) to explicitly nullable (string|null) - Added admin API routes and frontend UI for triggering onboarding and strategy suggestion workflows
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/schemas/strategy-parsers.ts | Changed description field from optional to nullable for explicit schema handling |
| packages/core/src/schema/arktype-json-schema-transformer.ts | New utility to transform enum fields to anyOf format in JSON schemas |
| packages/api-seo/src/lib/ai/arktype-json-schema.ts | Main transformer combining enum conversion with additionalProperties enforcement |
| packages/api-seo/src/workflows/writer-workflow.ts | Applied arktypeToAiJsonSchema wrapper to all Output.object schemas |
| packages/api-seo/src/workflows/strategy-suggestions-workflow.ts | Applied schema wrapper and added try-catch for error logging |
| packages/api-seo/src/workflows/strategy-phase-generation-workflow.ts | Applied schema wrapper to phase suggestion output |
| packages/api-seo/src/workflows/planner-workflow.ts | Applied schema wrapper to planning workflow outputs |
| packages/api-seo/src/workflows/onboarding-workflow.ts | Applied schema wrapper to onboarding workflow outputs |
| packages/api-seo/src/lib/ai/tools/settings-tools.ts | Applied schema wrapper to settings modification outputs |
| packages/api-seo/src/lib/ai/tools/google-search-console-tool.ts | Applied schema wrapper to GSC tool input schema |
| packages/api-seo/src/routes/admin.ts | New admin endpoints for triggering workflows with email domain-based auth |
| packages/api-seo/src/routes/index.ts | Registered admin router |
| apps/seo/src/routes/_authed/admin/route.tsx | Frontend admin UI for workflow triggering |
| apps/seo/src/routeTree.gen.ts | Auto-generated route tree with new admin route |
| if (jsonSchema.type === "object") { | ||
| jsonSchema.additionalProperties = false; | ||
| const properties = jsonSchema.properties; | ||
| if (properties != null) { | ||
| for (const property in properties) { | ||
| properties[property] = addAdditionalPropertiesToJsonSchema( | ||
| properties[property] as JSONSchema7, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| if (jsonSchema.type === "array" && jsonSchema.items != null) { | ||
| if (Array.isArray(jsonSchema.items)) { | ||
| jsonSchema.items = jsonSchema.items.map((item) => | ||
| addAdditionalPropertiesToJsonSchema(item as JSONSchema7), | ||
| ); | ||
| } else { | ||
| jsonSchema.items = addAdditionalPropertiesToJsonSchema( | ||
| jsonSchema.items as JSONSchema7, | ||
| ); | ||
| } | ||
| } | ||
| if (jsonSchema.anyOf != null) { | ||
| jsonSchema.anyOf = jsonSchema.anyOf.map((schema) => | ||
| addAdditionalPropertiesToJsonSchema(schema as JSONSchema7), | ||
| ); | ||
| } | ||
| if (jsonSchema.allOf != null) { | ||
| jsonSchema.allOf = jsonSchema.allOf.map((schema) => | ||
| addAdditionalPropertiesToJsonSchema(schema as JSONSchema7), | ||
| ); | ||
| } | ||
| if (jsonSchema.oneOf != null) { | ||
| jsonSchema.oneOf = jsonSchema.oneOf.map((schema) => | ||
| addAdditionalPropertiesToJsonSchema(schema as JSONSchema7), | ||
| ); | ||
| } | ||
| return jsonSchema; |
There was a problem hiding this comment.
The function mutates the input jsonSchema object instead of creating a new object. This can cause unexpected side effects if the same schema object is reused. Consider creating a deep copy at the start or using spread operators to avoid mutations.
| if (jsonSchema.type === "object") { | |
| jsonSchema.additionalProperties = false; | |
| const properties = jsonSchema.properties; | |
| if (properties != null) { | |
| for (const property in properties) { | |
| properties[property] = addAdditionalPropertiesToJsonSchema( | |
| properties[property] as JSONSchema7, | |
| ); | |
| } | |
| } | |
| } | |
| if (jsonSchema.type === "array" && jsonSchema.items != null) { | |
| if (Array.isArray(jsonSchema.items)) { | |
| jsonSchema.items = jsonSchema.items.map((item) => | |
| addAdditionalPropertiesToJsonSchema(item as JSONSchema7), | |
| ); | |
| } else { | |
| jsonSchema.items = addAdditionalPropertiesToJsonSchema( | |
| jsonSchema.items as JSONSchema7, | |
| ); | |
| } | |
| } | |
| if (jsonSchema.anyOf != null) { | |
| jsonSchema.anyOf = jsonSchema.anyOf.map((schema) => | |
| addAdditionalPropertiesToJsonSchema(schema as JSONSchema7), | |
| ); | |
| } | |
| if (jsonSchema.allOf != null) { | |
| jsonSchema.allOf = jsonSchema.allOf.map((schema) => | |
| addAdditionalPropertiesToJsonSchema(schema as JSONSchema7), | |
| ); | |
| } | |
| if (jsonSchema.oneOf != null) { | |
| jsonSchema.oneOf = jsonSchema.oneOf.map((schema) => | |
| addAdditionalPropertiesToJsonSchema(schema as JSONSchema7), | |
| ); | |
| } | |
| return jsonSchema; | |
| // Create a shallow copy so we don't mutate the input schema object. | |
| const updated: JSONSchema7 = { ...jsonSchema }; | |
| if (updated.type === "object") { | |
| updated.additionalProperties = false; | |
| const properties = updated.properties; | |
| if (properties != null) { | |
| const newProperties: typeof properties = {}; | |
| for (const property in properties) { | |
| if (Object.prototype.hasOwnProperty.call(properties, property)) { | |
| newProperties[property] = addAdditionalPropertiesToJsonSchema( | |
| properties[property] as JSONSchema7, | |
| ); | |
| } | |
| } | |
| updated.properties = newProperties; | |
| } | |
| } | |
| if (updated.type === "array" && updated.items != null) { | |
| if (Array.isArray(updated.items)) { | |
| updated.items = updated.items.map((item) => | |
| addAdditionalPropertiesToJsonSchema(item as JSONSchema7), | |
| ); | |
| } else { | |
| updated.items = addAdditionalPropertiesToJsonSchema( | |
| updated.items as JSONSchema7, | |
| ); | |
| } | |
| } | |
| if (updated.anyOf != null) { | |
| updated.anyOf = updated.anyOf.map((schema) => | |
| addAdditionalPropertiesToJsonSchema(schema as JSONSchema7), | |
| ); | |
| } | |
| if (updated.allOf != null) { | |
| updated.allOf = updated.allOf.map((schema) => | |
| addAdditionalPropertiesToJsonSchema(schema as JSONSchema7), | |
| ); | |
| } | |
| if (updated.oneOf != null) { | |
| updated.oneOf = updated.oneOf.map((schema) => | |
| addAdditionalPropertiesToJsonSchema(schema as JSONSchema7), | |
| ); | |
| } | |
| return updated; |
| function replaceStringEnumWithAnyOf(value: unknown): unknown { | ||
| if (Array.isArray(value)) { | ||
| return value.map((item) => replaceStringEnumWithAnyOf(item)); | ||
| } | ||
|
|
||
| if (!value || typeof value !== "object") { | ||
| return value; | ||
| } | ||
|
|
||
| const record = value as Record<string, unknown>; | ||
| const normalized: Record<string, unknown> = {}; | ||
|
|
||
| for (const [key, item] of Object.entries(record)) { | ||
| normalized[key] = replaceStringEnumWithAnyOf(item); | ||
| } | ||
|
|
||
| if ( | ||
| Array.isArray(record.enum) && | ||
| record.enum.every((item) => typeof item === "string") | ||
| ) { | ||
| normalized.anyOf = [...record.enum]; | ||
| normalized.type = "string"; | ||
| delete normalized.enum; | ||
| } | ||
| if (Array.isArray(record.anyOf)) { | ||
| normalized.type = "string"; | ||
| } | ||
|
|
||
| return normalized; | ||
| } |
There was a problem hiding this comment.
The replaceStringEnumWithAnyOf function mutates the record object and its nested properties. This could cause issues if the input schema is reused elsewhere. Consider using immutable operations or creating a deep copy of the input before transformation.
| const projectResult = await db.query.seoProject.findFirst({ | ||
| where: eq(schema.seoProject.slug, input.projectSlug), | ||
| }); | ||
|
|
||
| if (!projectResult) { | ||
| throw new ORPCError("NOT_FOUND", { message: "Project not found" }); | ||
| } |
There was a problem hiding this comment.
The admin endpoint allows triggering workflows for any project without verifying the admin user has permission to access that organization's project. While this is an internal admin endpoint, consider adding organization membership validation or at least logging which admin triggered which project's workflow for audit purposes.
|
|
||
| function RouteComponent() { | ||
| const api = getApiClientRq(); | ||
| const [projectSlug, setProjectSlug] = useState(""); |
There was a problem hiding this comment.
Both forms share the same projectSlug state variable. When a user enters a project slug in one form, it will appear in both forms. This creates a confusing user experience. Each form should have its own independent state variable for the project slug.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c3f4aebe46
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Cloudflare Preview URL for WWW 🎈 : https://pr-399.rectangularlabs.com (custom domain) |
No description provided.