Many fixes and improvements

This commit is contained in:
mijauexe
2025-02-20 15:50:53 +01:00
parent 26b1c1c576
commit e85e970637
12 changed files with 259 additions and 83 deletions

View File

@@ -1,9 +1,13 @@
from urllib.parse import urljoin
import httpx
from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
from core.agents.git import GitMixin
from core.agents.mixins import FileDiffMixin
from core.agents.response import AgentResponse
from core.config import FRONTEND_AGENT_NAME
from core.config import FRONTEND_AGENT_NAME, SWAGGER_EMBEDDINGS_API
from core.llm.parser import DescriptiveCodeBlockParser
from core.log import get_logger
from core.telemetry import telemetry
@@ -25,6 +29,8 @@ class Frontend(FileDiffMixin, GitMixin, BaseAgent):
async def run(self) -> AgentResponse:
if not self.current_state.epics[0]["messages"]:
finished = await self.start_frontend()
elif len(self.next_state.epics[-1].get("file_paths_to_remove_mock")) > 0:
finished = await self.remove_mock()
elif not self.next_state.epics[-1].get("fe_iteration_done"):
finished = await self.continue_frontend()
else:
@@ -50,6 +56,7 @@ class Frontend(FileDiffMixin, GitMixin, BaseAgent):
if self.state_manager.template is not None
else self.next_state.epics[0]["description"],
user_feedback=None,
first_time_build = True
)
response = await llm(convo, parser=DescriptiveCodeBlockParser())
response_blocks = response.blocks
@@ -140,11 +147,36 @@ class Frontend(FileDiffMixin, GitMixin, BaseAgent):
await self.send_message("Implementing the changes you suggested...")
llm = self.get_llm(FRONTEND_AGENT_NAME, stream_output=True)
convo = AgentConvo(self).template(
"is_relevant_for_docs_search",
user_feedback=answer.text,
)
response = await llm(convo)
relevant_api_documentation = None
if response == "yes":
try:
url = urljoin(SWAGGER_EMBEDDINGS_API, "search")
async with httpx.AsyncClient(transport=httpx.AsyncHTTPTransport(retries=3)) as client:
resp = await client.post(
url,
json={"text": answer.text, "project_id": str(self.state_manager.project.id), "user_id": "1"},
)
relevant_api_documentation = "\n".join(item["content"] for item in resp.json())
except Exception as e:
log.warning(f"Failed to fetch from RAG service: {e}", exc_info=True)
convo = AgentConvo(self).template(
"build_frontend",
description=self.current_state.epics[0]["description"],
user_feedback=answer.text,
relevant_api_documentation=relevant_api_documentation,
first_time_build=False,
)
response = await llm(convo, parser=DescriptiveCodeBlockParser())
await self.process_response(response.blocks)
@@ -187,7 +219,7 @@ class Frontend(FileDiffMixin, GitMixin, BaseAgent):
return AgentResponse.done(self)
async def process_response(self, response_blocks: list) -> AgentResponse:
async def process_response(self, response_blocks: list, removed_mock: bool = False) -> list[str]:
"""
Processes the response blocks from the LLM.
@@ -216,6 +248,12 @@ class Frontend(FileDiffMixin, GitMixin, BaseAgent):
await self.ui.generate_diff(
file_path, old_content, new_content, n_new_lines, n_del_lines, source=self.ui_source
)
if not removed_mock:
if "client/src/api" in file_path:
if not self.next_state.epics[-1].get("file_paths_to_remove_mock"):
self.next_state.epics[-1]["file_paths_to_remove_mock"] = []
self.next_state.epics[-1]["file_paths_to_remove_mock"].append(file_path)
await self.state_manager.save_file(file_path, new_content)
elif "command:" in last_line:
@@ -234,6 +272,47 @@ class Frontend(FileDiffMixin, GitMixin, BaseAgent):
return AgentResponse.done(self)
async def remove_mock(self) -> bool:
"""
Remove mock API from the backend and replace it with api endpoints defined in the external documentation
"""
new_file_paths = self.current_state.epics[-1]["file_paths_to_remove_mock"]
llm = self.get_llm(FRONTEND_AGENT_NAME)
for file_path in new_file_paths:
old_content = self.current_state.get_file_content_by_path(file_path)
convo = AgentConvo(self).template("create_rag_query", file_content=old_content)
topics = await llm(convo)
if topics != "None":
try:
url = urljoin(SWAGGER_EMBEDDINGS_API, "search")
async with httpx.AsyncClient(transport=httpx.AsyncHTTPTransport(retries=3)) as client:
resp = await client.post(
url, json={"text": topics, "project_id": str(self.state_manager.project.id), "user_id": "1"}
)
resp_json = resp.json()
relevant_api_documentation = "\n".join(item["content"] for item in resp_json)
convo = AgentConvo(self).template(
"remove_mock",
relevant_api_documentation=relevant_api_documentation,
file_content=old_content,
file_path=file_path,
lines=len(old_content.splitlines()),
)
response = await llm(convo, parser=DescriptiveCodeBlockParser())
response_blocks = response.blocks
convo.assistant(response.original_response)
await self.process_response(response_blocks, removed_mock=True)
self.next_state.epics[-1]["file_paths_to_remove_mock"].remove(file_path)
except Exception as e:
log.warning(f"Failed to fetch from RAG service: {e}", exc_info=True)
return False
async def set_app_details(self):
"""
Sets the app details.

View File

@@ -1,12 +1,15 @@
import json
import secrets
from json import JSONDecodeError
from urllib.parse import urljoin
from uuid import uuid4
import httpx
import yaml
from core.agents.base import BaseAgent
from core.agents.response import AgentResponse
from core.config import SWAGGER_EMBEDDINGS_API
from core.log import get_logger
from core.templates.registry import PROJECT_TEMPLATES
from core.ui.base import ProjectStage
@@ -18,31 +21,29 @@ class Wizard(BaseAgent):
agent_type = "wizard"
display_name = "Wizard"
# class LoginType(Enum):
# API_KEY = 0
# HTTP_AUTH = 1
# OAUTH2 = 2
# OPENID = 3
def get_auth_methods(self, docs: str) -> dict[str, any]:
def load_docs(self, docs: str) -> dict[str, any]:
try:
content = json.loads(docs)
return json.loads(docs)
except JSONDecodeError:
try:
content = yaml.safe_load(docs)
return yaml.safe_load(docs)
except Exception as e:
log.error(f"An error occurred: {str(e)}")
return {}
def get_auth_methods(self, docs: dict[str, any]) -> dict[str, any]:
auth_methods = {}
if "components" in content and "securitySchemes" in content["components"]:
auth_methods["types"] = [details["type"] for details in content["components"]["securitySchemes"].values()]
if "components" in docs and "securitySchemes" in docs["components"]:
auth_methods["types"] = [details["type"] for details in docs["components"]["securitySchemes"].values()]
auth_methods["api_version"] = 3
auth_methods["external_api_url"] = docs.get("servers", [{}])[0].get("url", "")
elif "securityDefinitions" in content:
auth_methods["types"] = [details["type"] for details in content["securityDefinitions"].values()]
elif "securityDefinitions" in docs:
auth_methods["types"] = [details["type"] for details in docs["securityDefinitions"].values()]
auth_methods["api_version"] = 2
auth_methods["external_api_url"] = (
"https://" + docs.get("host", "api.example.com") + docs.get("basePath", "")
)
return auth_methods
def create_custom_buttons(self, auth_methods):
@@ -85,10 +86,32 @@ class Wizard(BaseAgent):
allow_empty=False,
verbose=True,
)
auth_methods = self.get_auth_methods(docs.text.strip())
self.next_state.knowledge_base["docs"] = json.loads(docs.text.strip())
content = self.load_docs(docs.text.strip())
auth_methods = self.get_auth_methods(content)
self.next_state.knowledge_base["docs"] = content
self.next_state.knowledge_base["docs"]["api_version"] = auth_methods["api_version"]
self.next_state.knowledge_base["docs"]["external_api_url"] = auth_methods["external_api_url"]
try:
url = urljoin(SWAGGER_EMBEDDINGS_API, "upload")
async with httpx.AsyncClient(
transport=httpx.AsyncHTTPTransport(retries=3), timeout=httpx.Timeout(30.0, connect=60.0)
) as client:
await client.post(
url,
json={
"text": docs.text.strip(),
"project_id": str(self.state_manager.project.id),
"user_id": "1",
},
)
except Exception as e:
log.warning(f"Failed to fetch from RAG service: {e}", exc_info=True)
break
except Exception as e:
log.debug(f"An error occurred: {str(e)}")
@@ -128,9 +151,9 @@ class Wizard(BaseAgent):
allow_empty=False,
verbose=True,
)
# self.next_state.knowledge_base["api_key"] = api_key.text.strip()
options["auth_type"] = "api_key"
options["api_key"] = api_key.text.strip()
options["external_api_url"] = self.next_state.knowledge_base["docs"]["external_api_url"]
elif auth_type_question.button == "basic":
raise NotImplementedError()
elif auth_type_question.button == "bearer":
@@ -140,7 +163,6 @@ class Wizard(BaseAgent):
elif auth_type_question.button == "oauth2":
raise NotImplementedError()
# elif self.state_manager.project.project_type == "node":
else:
auth_needed = await self.ask_question(
"Do you need authentication in your app (login, register, etc.)?",
@@ -152,8 +174,7 @@ class Wizard(BaseAgent):
default="no",
)
options = {
"auth": auth_needed.button
== "yes", # todo fix tests, search for "auth", also options.auth in templates
"auth": auth_needed.button == "yes",
"auth_type": "login",
"jwt_secret": secrets.token_hex(32),
"refresh_token_secret": secrets.token_hex(32),

View File

@@ -50,6 +50,7 @@ FRONTEND_AGENT_NAME = "Frontend"
# Endpoint for the external documentation
EXTERNAL_DOCUMENTATION_API = "http://docs-pythagora-io-439719575.us-east-1.elb.amazonaws.com"
SWAGGER_EMBEDDINGS_API = "http://localhost:8000"
class _StrictModel(BaseModel):

View File

@@ -30,7 +30,7 @@ Whenever you add an API request from the frontend, make sure to wrap the request
{% endif %}
** IMPORTANT - current implementation **
Pay close attention to the currently implemented files, and DO NOT tell me to implementat something that is already implemented. Similarly, do not change the current implementation if you think it is working correctly. It is not necessary for you to change files - you can leave the files as they are and just tell me that they are correctly implemented.
Pay close attention to the currently implemented files, and DO NOT tell me to implement something that is already implemented. Similarly, do not change the current implementation if you think it is working correctly. It is not necessary for you to change files - you can leave the files as they are and just tell me that they are correctly implemented.
~~END_OF_DEVELOPMENT_INSTRUCTIONS~~
~~DEVELOPMENT_PLAN~~

View File

@@ -27,7 +27,10 @@ IMPORTANT: Text needs to be readable and in positive typography space - this is
You must create all code for all pages of this website. If this is a some sort of a dashboard, put the navigation in the sidebar.
**IMPORTANT**
{% if first_time_build %}
Make sure to implement all functionality (button clicks, form submissions, etc.) and use mock data for all interactions to make the app look and feel real. **ALL MOCK DATA MUST** be in the `api/` folder and it **MUST NOT** ever be hardcoded in the components.
{% endif %}
The body content should not overlap with the header navigation bar or footer navigation bar or the side navigation bar.
{% if user_feedback %}
@@ -42,3 +45,11 @@ Now, start by writing all code that's needed to get the frontend built for this
IMPORTANT: When suggesting/making changes in the file you must provide full content of the file! Do not use placeholders, or comments, or truncation in any way, but instead provide the full content of the file even the parts that are unchanged!
When you want to run a command you must put `command:` before the command and then the command itself like shown in the examples in system prompt. NEVER run `npm run start` or `npm run dev` commands, user will run them after you provide the code. The user is using {{ os }}, so the commands must run on that operating system
{% if relevant_api_documentation is defined %}
Here is relevant API documentation you need to consult and follow as close as possible.
{{ relevant_api_documentation }}
{% endif %}

View File

@@ -0,0 +1,6 @@
{{ file_content }}
I have external documentation in Swagger Open API format.
Create a comma separated list of short words topics of the description of the file so that I can search for the relevant parts of the documentation.
Use up to 5 words max.
If the file does not need external API documentation, just return "None" and nothing else, no explanation needed.

View File

@@ -0,0 +1,4 @@
{{ user_feedback }}
Does this prompt require taking a look at the API documentation? For example, you would look at API documentation if you need to implement or edit an API request or response.
Reply with a yes or a no, nothing else.

View File

@@ -0,0 +1,17 @@
Now you need to remove mocked data from the file and replace it with real API requests.
Replace only mocked data, do not change any other part of the file.
{% if relevant_api_documentation is defined %}
Here is external documentation that you will need to properly implement real API requests:
~~~START_OF_DOCUMENTATION~~~
{{ relevant_api_documentation }}
~~~END_OF_DOCUMENTATION~~~
IMPORTANT: Do not implement backend server logic for this, as this is already available in the external API!
{% endif %}
This is the file that you need to change:
**`{{ file_path }}`** ({{ lines }} lines of code):
```
{{ file_content }}
```

View File

@@ -36,8 +36,6 @@ NEVER run `npm run start` or `npm run dev` commands, user will run them after yo
IMPORTANT: The order of the actions is very important. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file.
IMPORTANT: Make sure to implement all functionality (button clicks, form submissions, etc.) and use MOCK DATA for all interactions to make the app look and feel real. MOCK DATA should be used for all interactions.
IMPORTANT: Put full path of file you are editing! Mostly you will work with files inside "client/" folder so don't forget to put it in file path, for example DO `client/src/App.tsx` instead of `src/App.tsx`.
{% include "partials/file_naming.prompt" %}

View File

@@ -7,6 +7,7 @@ This app has 2 parts:
* Client-side routing using `react-router-dom` with page components defined in `client/src/pages/` and other components in `client/src/components`
* It is running on port 5173 and this port should be used for user testing when possible
* All requests to the backend need to go to an endpoint that starts with `/api/` (e.g. `/api/companies`)
* Server proxy configuration is already configured and should not be changed in any way!
* Implememented pages:
* Home - home (index) page (`/`){% if options.auth %}
* Login - login page (`/login/`) - on login, stores the auth tokens to `accessToken` and `refreshToken` variables in local storage

View File

@@ -2,9 +2,21 @@ import axios, { AxiosRequestConfig, AxiosError } from 'axios';
{% if options.auth_type == "api_key" %}
const API_KEY = import.meta.env.VITE_API_KEY;
const EXTERNAL_API_URL = import.meta.env.VITE_EXTERNAL_API_URL;
{% endif %}
const api = axios.create({
// Create two axios instances
const localApi = axios.create({
headers: {
'Content-Type': 'application/json',
},
validateStatus: (status) => {
return status >= 200 && status < 300;
},
});
const externalApi = axios.create({
baseURL: EXTERNAL_API_URL,
headers: {
'Content-Type': 'application/json',
},
@@ -16,81 +28,107 @@ const api = axios.create({
let accessToken: string | null = null;
{% if options.auth %}
// Axios request interceptor: Attach access token and API key to headers
api.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
// Check if the request is not for login or register
const isAuthEndpoint = (url: string): boolean => {
return url.includes("/api/auth");
};
const getApiInstance = (url: string) => {
return isAuthEndpoint(url) ? localApi : externalApi;
};
// Interceptor for both API instances
const setupInterceptors = (apiInstance: typeof axios) => {
apiInstance.interceptors.request.use(
(config: AxiosRequestConfig): AxiosRequestConfig => {
{% if options.auth_type == "api_key" %}
const isAuthEndpoint = config.url?.includes('/login') || config.url?.includes('/register');
if (!isAuthEndpoint) {
// Add API key for non-auth endpoints
if (config.headers && API_KEY) {
config.headers['api_key'] = API_KEY; // or whatever header name your API expects
}
if (!isAuthEndpoint(config.url || '')) {
config.baseURL = EXTERNAL_API_URL;
if (config.headers && API_KEY) {
config.headers['api_key'] = API_KEY;
}
}
{% endif %}
// Add authorization token if available
if (!accessToken) {
accessToken = localStorage.getItem('accessToken');
}
if (accessToken && config.headers) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
}
return config;
},
(error: AxiosError): Promise<AxiosError> => Promise.reject(error)
);
return config;
},
(error: AxiosError): Promise<AxiosError> => Promise.reject(error)
);
// Axios response interceptor: Handle 401 errors
api.interceptors.response.use(
(response) => response, // If the response is successful, return it
async (error: AxiosError): Promise<any> => {
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
apiInstance.interceptors.response.use(
(response) => response,
async (error: AxiosError): Promise<any> => {
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
// If the error is due to an expired access token
if ([401, 403].includes(error.response?.status) && !originalRequest._retry) {
originalRequest._retry = true; // Mark the request as retried
if ([401, 403].includes(error.response?.status) && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Attempt to refresh the token
const { data } = await axios.post(`/api/auth/refresh`, {
refreshToken: localStorage.getItem('refreshToken'),
});
accessToken = data.data.accessToken;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', data.data.refreshToken);
try {
if (isAuthEndpoint(originalRequest.url || '')) {
const { data } = await localApi.post(`/api/auth/refresh`, {
refreshToken: localStorage.getItem('refreshToken'),
});
accessToken = data.data.accessToken;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', data.data.refreshToken);
}
// Retry the original request with the new token
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
// Ensure API key is still present in retry
{% if options.auth_type == "api_key" %}
if (API_KEY) {
originalRequest.headers['api_key'] = API_KEY;
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
{% if options.auth_type == "api_key" %}
if (!isAuthEndpoint(originalRequest.url || '') && API_KEY) {
originalRequest.headers['api_key'] = API_KEY;
}
{% endif %}
}
{% endif %}
return getApiInstance(originalRequest.url || '')(originalRequest);
} catch (err) {
localStorage.removeItem('refreshToken');
localStorage.removeItem('accessToken');
accessToken = null;
window.location.href = '/login';
return Promise.reject(err);
}
return api(originalRequest);
} catch (err) {
// If refresh fails, clear tokens and redirect to login
localStorage.removeItem('refreshToken');
localStorage.removeItem('accessToken');
accessToken = null;
window.location.href = '/login'; // Redirect to login page
return Promise.reject(err);
}
}
return Promise.reject(error); // Pass other errors through
}
);
return Promise.reject(error);
}
);
};
// Setup interceptors for both API instances
setupInterceptors(localApi);
setupInterceptors(externalApi);
{% endif %}
// Export a wrapper function that chooses the appropriate API instance
const api = {
request: (config: AxiosRequestConfig) => {
const apiInstance = getApiInstance(config.url || '');
return apiInstance(config);
},
get: (url: string, config?: AxiosRequestConfig) => {
const apiInstance = getApiInstance(url);
return apiInstance.get(url, config);
},
post: (url: string, data?: any, config?: AxiosRequestConfig) => {
const apiInstance = getApiInstance(url);
return apiInstance.post(url, data, config);
},
put: (url: string, data?: any, config?: AxiosRequestConfig) => {
const apiInstance = getApiInstance(url);
return apiInstance.put(url, data, config);
},
delete: (url: string, config?: AxiosRequestConfig) => {
const apiInstance = getApiInstance(url);
return apiInstance.delete(url, config);
},
};
export default api;

View File

@@ -83,7 +83,7 @@ class ViteReactProjectTemplate(BaseProjectTemplate):
"client/tsconfig.app.json": "TypeScript configuration for application code.",
"client/tsconfig.json": "Main TypeScript configuration with project references.",
"client/tsconfig.node.json": "TypeScript configuration for Node.js environment.",
"client/vite.config.ts": "Vite build tool configuration with React plugin and aliases.",
"client/vite.config.ts": "Vite build tool configuration with server proxy configuration.",
"client/tailwind.config.js": "Tailwind CSS configuration with theme customizations, including enabling dark mode, specifying the content files that Tailwind should scan for class names, and extending the default theme with custom values for border radius, colors, keyframes, and animations. The configuration also includes a plugin for animations, specifically 'tailwindcss-animate', which allows for additional animation utilities to be used in the project.",
"server/.env": "This file is a configuration file in the form of a .env file. It contains environment variables used by the application, such as the port to listen on, the MongoDB database URL, and the session secret string.",
"server/server.js": "This `server.js` file sets up an Express server with MongoDB database connection, session management using connect-mongo, templating engine EJS, static file serving, authentication routes, error handling, and request logging. [References: dotenv, mongoose, express, express-session, connect-mongo, ./routes/authRoutes]",