├── .env.example ├── requirements.txt ├── .gitattributes ├── .gitignore ├── main.py ├── lib ├── tools.py ├── files.py └── agent.py ├── app.py └── readme.md /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=sk-proj-12345 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | openai-agents 2 | openai 3 | streamlit 4 | pydantic 5 | asyncio -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.local 3 | .env.development.local 4 | .env.test.local 5 | .env.production.local 6 | .env.development 7 | .env.test 8 | .env.production 9 | 10 | .venv 11 | venv 12 | env/ 13 | ENV/ 14 | 15 | __pycache__ 16 | 17 | resources/* 18 | output/* 19 | 20 | .DS_Store -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from lib.agent import run_agent 3 | 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | async def main(): 9 | 10 | result = await run_agent("1920s Art Deco", "example.jpg") 11 | 12 | print("Final output:", result["final_output"]) 13 | print("Image paths:", result["image_paths"]) 14 | 15 | if __name__ == "__main__": 16 | 17 | asyncio.run(main()) -------------------------------------------------------------------------------- /lib/tools.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from agents import function_tool 3 | 4 | class DesignDatabaseEntry(BaseModel): 5 | rooms: list[str] 6 | design_style: str 7 | color_palette: list[str] 8 | furniture: list[str] 9 | 10 | @function_tool 11 | async def save_design_data_to_database(data: DesignDatabaseEntry): 12 | # Simulate a database save with an async sleep 13 | print("Design data saved to database:", data) 14 | 15 | with open("output/design_output.txt", "w") as f: 16 | f.write(str(data)) -------------------------------------------------------------------------------- /lib/files.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | def open_file(path: str) -> None: 7 | if sys.platform.startswith("darwin"): 8 | subprocess.run(["open", path], check=False) # macOS 9 | elif os.name == "nt": # Windows 10 | os.startfile(path) # type: ignore 11 | elif os.name == "posix": 12 | subprocess.run(["xdg-open", path], check=False) # Linux/Unix 13 | else: 14 | print(f"Don't know how to open files on this platform: {sys.platform}") 15 | 16 | def retrieve_image_from_resources(path: str): 17 | # If path is absolute, use as is. If not, prepend resources/ 18 | if not os.path.isabs(path): 19 | path = os.path.join("resources", path) 20 | with open(path, "rb") as f: 21 | base64_image = base64.b64encode(f.read()).decode("utf-8") 22 | return base64_image -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import asyncio 3 | from lib.agent import run_agent 4 | import tempfile 5 | 6 | from dotenv import load_dotenv 7 | 8 | load_dotenv() 9 | 10 | st.title("Interior Designer Agent") 11 | 12 | style = st.text_input("Describe your intended interior design styling:") 13 | image_file = st.file_uploader("Upload an image (PNG or JPG)", type=["png", "jpg", "jpeg"]) 14 | 15 | if st.button("Run Agent"): 16 | if style and image_file: 17 | with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_file: 18 | tmp_file.write(image_file.read()) 19 | tmp_file_path = tmp_file.name 20 | 21 | # Display the uploaded floorplan image 22 | st.image(image_file, caption="Uploaded Floorplan", use_container_width=True) 23 | st.write("Running agent...") 24 | # Run the agent asynchronously 25 | result = asyncio.run(run_agent(style, tmp_file_path)) 26 | if result: 27 | st.markdown(f"**Response:** {result['final_output'].description_of_interior_design}") 28 | if 'image_paths' in result: 29 | for img_path in result['image_paths']: 30 | st.image(img_path, caption=img_path, use_container_width=True) 31 | else: 32 | st.error("No result returned from agent.") 33 | else: 34 | st.error("Please provide both a style description and an image.") 35 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Interior Designer Agent 2 | 3 | AI-powered Interior Designer Agent — a minimal, open-source MVP for generating interior design images and ideas using LLMs and image models. 4 | 5 | This repository contains: 6 | 7 | - **Streamlit App** (`app.py`) — a simple UI for generating and viewing interior design images. 8 | - **Agent Core** (`lib/agent.py`) — the main agent logic for generating design suggestions and images. 9 | - **File Management** (`lib/files.py`) — handles file operations for generated images and resources. 10 | - **Output Images** (`output/`) — stores generated images and their low-res versions. 11 | - **Resources** (`resources/`) — example images and floorplans for reference or input. 12 | 13 | ## 🚀 Features 14 | 15 | - Generate interior design images using AI models 16 | - Upload and use reference images or floorplans 17 | - Simple Streamlit interface for easy interaction 18 | - Extensible agent logic for custom design workflows 19 | 20 | ## 🛠️ Setup 21 | 22 | ### Prerequisites 23 | 24 | - Python 3.11+ 25 | - API keys for any external model providers (only OpenAI in this case) 26 | 27 | ### Installation 28 | 29 | 1. Clone the repo: 30 | 31 | ```bash 32 | git clone 33 | cd interior-designer-agent 34 | ``` 35 | 36 | 2. Install dependencies: 37 | 38 | ```bash 39 | pip install -r requirements.txt 40 | ``` 41 | 42 | 3. Create your `.env` file (copy `.env.example`): 43 | 44 | ```bash 45 | cp .env.example .env 46 | ``` 47 | 48 | 4. Set required environment variables in `.env`: 49 | 50 | ``` 51 | OPENAI_API_KEY=sk-... # for LLM usage 52 | ``` 53 | 54 | Refer to `.env.example` for all available variables. You must provide valid API keys for any model providers you use. **There is no defined budget cap — users will likely incur costs associated with running the models.** 55 | 56 | ### Running the Project 57 | 58 | Start the Streamlit app: 59 | 60 | ```bash 61 | streamlit run app.py 62 | ``` 63 | 64 | Generated images will be saved in the `output/` directory.. 65 | 66 | Alternatively you can run the project using: 67 | 68 | ```bash 69 | python main.py 70 | ``` 71 | 72 | ## 💸 Cost Warning 73 | 74 | **Important:** This agent does not enforce any budget cap. By default, users will likely incur costs when using external model APIs. Monitor your API usage and set limits with your provider if needed. 75 | 76 | ## 🏗️ Architecture (high-level) 77 | 78 | 1. `app.py` — Streamlit UI for generating and viewing images. 79 | 2. `lib/agent.py` — agent logic for design suggestions and image generation. 80 | 3. `lib/files.py` — file management utilities. 81 | 4. `output/` — stores generated images. 82 | 83 | ## 🤖 Usage Example 84 | 85 | - Upload a floorplan or reference image via the Streamlit UI. 86 | - Enter your design prompt (e.g., "1920s modern luxury"). 87 | - The agent generates images and saves them to `output/`. 88 | - View and download generated images from the UI. 89 | 90 | ## 🤝 Contributing 91 | 92 | 1. Fork the repository 93 | 2. Create a feature branch 94 | 3. Run tests and linters 95 | 4. Open a pull request with a description of your changes 96 | 97 | Please include a short description of how your change was tested. 98 | 99 | --- 100 | 101 | Built with ❤️ by [Tom Shaw](https://tomshaw.dev) 102 | -------------------------------------------------------------------------------- /lib/agent.py: -------------------------------------------------------------------------------- 1 | from agents import Agent, Runner, ImageGenerationTool, input_guardrail, GuardrailFunctionOutput, InputGuardrailTripwireTriggered, RunContextWrapper 2 | from lib.files import retrieve_image_from_resources, open_file 3 | import base64 4 | from pydantic import BaseModel 5 | 6 | from lib.tools import save_design_data_to_database 7 | import os 8 | 9 | class GuardrailAgentOutput(BaseModel): 10 | is_not_allowed: bool 11 | reason: str | None 12 | 13 | guardrail_agent = Agent( 14 | name="Floorplan Checker Agent", 15 | instructions="""" 16 | Check if the image that the user has submitted is a valid floorplan and that the user's design preference input is actually relevant to interior design. The user must not ask for anything offensive or not safe for work. 17 | """, 18 | tools=[], 19 | output_type=GuardrailAgentOutput 20 | ) 21 | 22 | @input_guardrail 23 | async def guardrail_function(ctx: RunContextWrapper, agent: Agent, input_data: str) -> GuardrailFunctionOutput: 24 | result = await Runner.run(guardrail_agent, input_data) 25 | 26 | return GuardrailFunctionOutput( 27 | output_info=result.final_output.reason, 28 | tripwire_triggered=result.final_output.is_not_allowed 29 | ) 30 | 31 | class DesignOutput(BaseModel): 32 | rooms: list[str] 33 | design_style: str 34 | color_palette: list[str] 35 | furniture: list[str] 36 | description_of_interior_design: str 37 | 38 | my_agent = Agent( 39 | name="Interior Design Agent", 40 | instructions=""" 41 | You are an interior design agent that can generate design images for every room in a home based on the floorplan that is submitted by the user. 42 | 43 | You should approach the problem using this process: 44 | 1. Identify the rooms in the floorplan image submitted by the user. 45 | 2. Identify the realistic dimensions of each of the rooms in the floorplan. 46 | 3. Plan the layout and design elements for each room based on the user's preferences (take into account the placement of fixed features such as doors and windows which are visible in the floorplan) 47 | 4. Generate 1 image for each room based on the design plan. 48 | 5. Save the interior design details into the database for each room that you have generated. Only save the details of the design after you have generated the image. Save the interior for this entire floorplan in a single database entry. Do not make multiple database entries. You must use the tool `save_design_data_to_database` to do this. 49 | 50 | The user's design preferences will be submitted using the user prompt. 51 | 52 | Image Generation guidelines: 53 | - Ensure that the images are relevant to the floorplan 54 | - Ensure that the images are from a camera perspective that showcases the entire room 55 | - Do not add in windows or doors where they do not exist in the floorplan 56 | - Do not generate individual images of hallways (these are not required) 57 | - Only generate a maximum of 5 images in total 58 | 59 | Output: 60 | You should return the final output of the agent, including the generated images. Do not output text links to the images in the final output. 61 | """, 62 | tools=[ 63 | ImageGenerationTool({ 64 | "type": "image_generation", 65 | "output_format": "png", 66 | "quality": "low", 67 | "size": "1024x1024" 68 | }), 69 | save_design_data_to_database 70 | ], 71 | model="gpt-4.1", 72 | input_guardrails=[ 73 | guardrail_function 74 | ], 75 | output_type=DesignOutput 76 | ) 77 | 78 | async def run_agent(design_style: str, floorplan_image: str): 79 | 80 | os.makedirs("output", exist_ok=True) 81 | 82 | image = retrieve_image_from_resources(floorplan_image) 83 | 84 | formatted_input = [{ 85 | "role": "user", 86 | "content": [ 87 | { 88 | "type": "input_image", 89 | "image_url": f"data:image/jpeg;base64,{image}" 90 | }, 91 | { 92 | "type": "input_text", 93 | "text": design_style 94 | } 95 | ] 96 | }] 97 | 98 | try: 99 | 100 | 101 | result = await Runner.run(my_agent, formatted_input) 102 | 103 | print("Final output:", result.final_output) 104 | 105 | print("Full run details:", result) 106 | 107 | image_paths = [] 108 | 109 | image_count = 0 110 | 111 | for item in result.new_items: 112 | if ( 113 | item.type == "tool_call_item" 114 | and item.raw_item.type == "image_generation_call" 115 | and (img_result := item.raw_item.result) 116 | ): 117 | with open(f"output/generated_image_{image_count}.png", "wb") as f: 118 | f.write(base64.b64decode(img_result)) 119 | image_paths.append(f"output/generated_image_{image_count}.png") 120 | image_count += 1 121 | open_file(f"output/generated_image_{image_count}.png") 122 | 123 | return { 124 | "image_paths": image_paths, 125 | "final_output": result.final_output 126 | } 127 | 128 | except InputGuardrailTripwireTriggered as e: 129 | print("Input guardrail triggered:", e) --------------------------------------------------------------------------------