We are going to build an architectural concept assistant that reads a client brief, analyzes a site photograph, and produces a structured spatial program with design rationale. It helps solo practitioners and small firms automate early-stage schematic thinking without replacing the architect's judgment.
What you'll need
Python 3.10 or newer, the OpenAI SDK installed with pip install openai, and an Oxlo.ai API key from https://portal.oxlo.ai. You will also need a site photo in JPEG or PNG format to analyze.
Step 1: Configure the client
Set up the OpenAI SDK to point at Oxlo.ai. I keep my key in an environment variable so it does not leak into source control.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key=os.environ.get("OXLO_API_KEY")
)
print("Client ready:", client.base_url)
Step 2: Write the system prompt
The system prompt defines the agent's role as a senior design architect. It constrains output to practical, buildable concepts and asks for structured reasoning. I treat this as the single most important file in the project.
SYSTEM_PROMPT = """You are a senior architectural designer acting as a concept assistant.
Your job is to help architects translate client briefs and site constraints into spatial strategies.
Rules:
- Always ground suggestions in the site context, climate, and brief.
- Favor passive design strategies before active systems.
- When analyzing images, describe circulation, solar orientation, and adjacencies you observe.
- Output structured data when requested, using exact field names.
- Do not invent zoning codes. If unknown, note the gap and suggest what to verify.
"""
Step 3: Analyze site photos with vision
We use Kimi K2.6 because it handles vision and long context. The agent receives the brief and a site photo, then returns observations about topography, access, and solar exposure. I encode the image as a base64 data URI.
import base64
def encode_image(image_path):
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def analyze_site(brief_text, image_path):
b64_image = encode_image(image_path)
data_uri = f"data:image/jpeg;base64,{b64_image}"
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": [
{"type": "text", "text": f"Client brief: {brief_text}"},
{"type": "image_url", "image_url": {"url": data_uri}},
{
"type": "text",
"text": (
"Analyze this site photo. Describe topography, apparent solar orientation, "
"access points, and any constraints that should shape the massing."
),
},
],
},
]
response = client.chat.completions.create(
model="kimi-k2.6",
messages=messages,
max_tokens=1024,
)
return response.choices[0].message.content
# Example usage
site_notes = analyze_site(
"Small mixed-use building on a corner lot. Needs ground-floor retail and two residential units above.",
"site_photo.jpg",
)
print(site_notes)
Step 4: Generate structured program data
Now we turn the brief and site notes into a structured spatial program. We use JSON mode so the downstream report generator can rely on field names. I use Llama 3.3 70B here because it follows instructions for structured output reliably.
import json
def generate_program(brief_text, site_notes):
user_content = (
f"Client brief: {brief_text}\n\n"
f"Site analysis notes: {site_notes}\n\n"
"Generate a spatial program as JSON with these exact top-level keys: "
"building_summary, spaces (list of objects with name, area_sqm, adjacencies, daylighting_strategy), "
"material_palette (list), sustainability_notes (list). "
"Do not include markdown fences."
)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_content},
],
response_format={"type": "json_object"},
max_tokens=2048,
)
raw = response.choices[0].message.content
return json.loads(raw)
program = generate_program(
"Small mixed-use building on a corner lot. Needs ground-floor retail and two residential units above.",
site_notes,
)
print(json.dumps(program, indent=2))
Step 5: Assemble the report
Finally, we combine everything into a readable markdown report. This function passes the JSON back through the LLM to write a narrative design rationale. I use Qwen 3 32B for fluent, structured prose.
def draft_report(brief_text, site_notes, program_json):
prompt = (
"Write a concise architectural concept report in markdown. "
"Include: 1) An executive summary, 2) Site response paragraph, 3) Spatial strategy paragraph, "
"4) A bullet list of spaces with areas, 5) Sustainability priorities. "
"Use the following data.\n\n"
f"Client brief: {brief_text}\n\n"
f"Site analysis: {site_notes}\n\n"
f"Spatial program JSON: {json.dumps(program_json)}\n"
)
response = client.chat.completions.create(
model="qwen-3-32b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
max_tokens=2048,
)
return response.choices[0].message.content
report = draft_report(
"Small mixed-use building on a corner lot. Needs ground-floor retail and two residential units above.",
site_notes,
program,
)
with open("concept_report.md", "w") as f:
f.write(report)
print("Report written to concept_report.md")
Run it
Here is the full script wired together. Save it as architect_agent.py, place a photo named site_photo.jpg in the same folder, and run python architect_agent.py.
import os
import json
import base64
from openai import OpenAI
client = OpenAI(
base_url="https://api.oxlo.ai/v1",
api_key=os.environ.get("OXLO_API_KEY")
)
SYSTEM_PROMPT = """You are a senior architectural designer acting as a concept assistant.
Your job is to help architects translate client briefs and site constraints into spatial strategies.
Rules:
- Always ground suggestions in the site context, climate, and brief.
- Favor passive design strategies before active systems.
- When analyzing images, describe circulation, solar orientation, and adjacencies you observe.
- Output structured data when requested, using exact field names.
- Do not invent zoning codes. If unknown, note the gap and suggest what to verify.
"""
def encode_image(image_path):
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def analyze_site(brief_text, image_path):
b64_image = encode_image(image_path)
data_uri = f"data:image/jpeg;base64,{b64_image}"
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": [
{"type": "text", "text": f"Client brief: {brief_text}"},
{"type": "image_url", "image_url": {"url": data_uri}},
{
"type": "text",
"text": (
"Analyze this site photo. Describe topography, apparent solar orientation, "
"access points, and any constraints that should shape the massing."
),
},
],
},
]
response = client.chat.completions.create(
model="kimi-k2.6",
messages=messages,
max_tokens=1024,
)
return response.choices[0].message.content
def generate_program(brief_text, site_notes):
user_content = (
f"Client brief: {brief_text}\n\n"
f"Site analysis notes: {site_notes}\n\n"
"Generate a spatial program as JSON with these exact top-level keys: "
"building_summary, spaces (list of objects with name, area_sqm, adjacencies, daylighting_strategy), "
"material_palette (list), sustainability_notes (list). "
"Do not include markdown fences."
)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_content},
],
response_format={"type": "json_object"},
max_tokens=2048,
)
raw = response.choices[0].message.content
return json.loads(raw)
def draft_report(brief_text, site_notes, program_json):
prompt = (
"Write a concise architectural concept report in markdown. "
"Include: 1) An executive summary, 2) Site response paragraph, 3) Spatial strategy paragraph, "
"4) A bullet list of spaces with areas, 5) Sustainability priorities. "
"Use the following data.\n\n"
f"Client brief: {brief_text}\n\n"
f"Site analysis: {site_notes}\n\n"
f"Spatial program JSON: {json.dumps(program_json)}\n"
)
response = client.chat.completions.create(
model="qwen-3-32b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
max_tokens=2048,
)
return response.choices[0].message.content
if __name__ == "__main__":
brief = (
"Small mixed-use building on a corner lot. "
"Needs ground-floor retail and two residential units above."
)
image_path = "site_photo.jpg"
print("Analyzing site...")
site_notes = analyze_site(brief, image_path)
print(site_notes[:500], "...\n")
print("Generating program...")
program = generate_program(brief, site_notes)
print("Drafting report...")
report = draft_report(brief, site_notes, program)
with open("concept_report.md", "w") as f:
f.write(report)
print("Done. See concept_report.md")
When I ran this against a corner lot photo, the site analysis began:
"The site appears relatively flat with a slight grade toward the southeast corner. The main street frontage faces southwest, suggesting afternoon solar gain on the primary facade. A narrow service alley runs along the north edge, offering secondary access and utility routing..."
The generated JSON contained:
{
"building_summary": "Three-story mixed-use infill on a tight corner lot",
"spaces": [
{
"name": "Ground-floor retail",
"area_sqm": 85,
"adjacencies": ["main street entry", "back of house"],
"daylighting_strategy": "Large south-facing glazing with deep overhang"
}
],
"material_palette": ["reclaimed brick", "cross-laminated timber", "powder-coated steel"],
"sustainability_notes": ["Passive solar shading via balcony overhangs", "Rainwater collection from flat roof"]
}
The final markdown report stitched these into a coherent two-page concept narrative.
Next steps
Wire the JSON program output into a Python script that generates a rough SketchUp or Blender mesh using the dimensions, turning the assistant into a geometry seeding tool. Alternatively, add a feedback loop where the architect edits the JSON and sends it back for a revised report, using Oxlo.ai's multi-turn context to maintain design continuity across iterations.
Because Oxlo.ai uses flat per-request pricing, running a multi-step agent pipeline with long briefs and image analysis does not inflate costs the way token-based providers do for long-context workloads. You can iterate freely. See https://oxlo.ai/pricing for plan details.
Top comments (0)