diff --git a/dev-ai-app-dev-constructioneng-aiexperience/build/build.md b/dev-ai-app-dev-constructioneng-aiexperience/build/build.md index 7ba56846c..98e20d441 100644 --- a/dev-ai-app-dev-constructioneng-aiexperience/build/build.md +++ b/dev-ai-app-dev-constructioneng-aiexperience/build/build.md @@ -255,7 +255,7 @@ Here’s what the code does: into a structured prompt. The prompt gives the LLM decision rules for when to use `APPROVE`, `REQUEST INFO`, or `DENY`. 💡 **`prompt = f"""`** * **Use OCI Generative AI**: Send the prompt to the - `meta.llama-3.2-90b-vision-instruct` model through OCI’s + `meta.llama-3.3-70b-instruct` model through OCI’s Generative AI inference client. 💡 **`chat_response = genai_client.chat(chat_detail)`** * **Display the Output**: Print the generated supplier evaluation using the same sections used in the Seer Construction app: @@ -271,6 +271,10 @@ so it can be used in the RAG flow. ```python + def read_lob(value): + return value.read() if isinstance(value, oracledb.LOB) else value + + cursor.execute( """ SELECT @@ -298,129 +302,162 @@ so it can be used in the RAG flow. """, {"project_id": selected_project_id} ) + + columns = [ + "EVALUATION_ID", + "RECOMMEND_ID", + "RECOMMENDATION", + "FIT_SCORE", + "RISK_LEVEL", + "EXPLANATION", + "STRENGTHS", + "MISSING_INFORMATION", + "SUPPLIER_ID", + "SUPPLIER_NAME", + "CATEGORY", + "REGION", + "CAPACITY_STATUS", + "CAPABILITY_SUMMARY" + ] + df_supplier_recommendations = pd.DataFrame( - cursor.fetchall(), - columns=[ - "EVALUATION_ID", - "RECOMMEND_ID", - "RECOMMENDATION", - "FIT_SCORE", - "RISK_LEVEL", - "EXPLANATION", - "STRENGTHS", - "MISSING_INFORMATION", - "SUPPLIER_ID", - "SUPPLIER_NAME", - "CATEGORY", - "REGION", - "CAPACITY_STATUS", - "CAPABILITY_SUMMARY" - ] + [[read_lob(value) for value in row] for row in cursor.fetchall()], + columns=columns ) + print(f"Loaded {len(df_supplier_recommendations)} supplier recommendation records.") + def generate_supplier_recommendations(project_id, project_json, df_supplier_recommendations): requirement = (project_json.get("requirements") or [{}])[0] evaluation = (project_json.get("supplierEvaluations") or [{}])[0] recommendation = evaluation.get("recommendation") or {} - available_data_text = "\n".join([ - ( - f"Supplier Evaluation {row['EVALUATION_ID']}: " - f"{row['SUPPLIER_NAME']} | Decision: {row['RECOMMENDATION']} | " - f"Fit Score: {row['FIT_SCORE']} | Risk: {row['RISK_LEVEL']} | " - f"Capacity: {row['CAPACITY_STATUS']} | " - f"Explanation: {row['EXPLANATION']} | " - f"Missing Information: {row['MISSING_INFORMATION']}" - ) - for row in df_supplier_recommendations.to_dict(orient="records") - ]) - - project_profile_text = "\n".join([ - f"- Project Name: {project_json.get('projectName', '')}", - f"- Location: {project_json.get('location', '')}", - f"- Project Type: {project_json.get('projectType', '')}", - f"- Project Phase: {project_json.get('projectPhase', '')}", - f"- Project Summary: {project_json.get('projectSummary', '')}", - f"- Required Trade: {requirement.get('tradeCategory', '')}", - f"- Material Need: {requirement.get('materialNeed', '')}", - f"- Required Certification: {requirement.get('requiredCertification', '')}", - f"- Delivery Window: {requirement.get('deliveryWindow', '')}", - f"- Procurement Urgency: {requirement.get('procurementUrgency', '')}", - f"- Budget Range: {requirement.get('budgetRange', '')}", - f"- Risk Level: {requirement.get('riskLevel', '')}", - f"- Current Evaluation Status: {evaluation.get('evaluationStatus', '')}", - f"- Current Recommended Supplier: {recommendation.get('supplier', {}).get('supplierName', '')}" - ]) + available_data_text = "\n".join( + [ + ( + f"Supplier Evaluation {row['EVALUATION_ID']}: " + f"{row['SUPPLIER_NAME']} | " + f"Decision: {row['RECOMMENDATION']} | " + f"Fit Score: {row['FIT_SCORE']} | " + f"Risk: {row['RISK_LEVEL']} | " + f"Capacity: {row['CAPACITY_STATUS']} | " + f"Explanation: {row['EXPLANATION']} | " + f"Missing Information: {row['MISSING_INFORMATION']}" + ) + for row in df_supplier_recommendations.to_dict(orient="records") + ] + ) + + project_profile_text = "\n".join( + [ + f"- Project Name: {project_json.get('projectName', '')}", + f"- Location: {project_json.get('location', '')}", + f"- Project Type: {project_json.get('projectType', '')}", + f"- Project Phase: {project_json.get('projectPhase', '')}", + f"- Project Summary: {project_json.get('projectSummary', '')}", + f"- Required Trade: {requirement.get('tradeCategory', '')}", + f"- Material Need: {requirement.get('materialNeed', '')}", + f"- Required Certification: {requirement.get('requiredCertification', '')}", + f"- Delivery Window: {requirement.get('deliveryWindow', '')}", + f"- Procurement Urgency: {requirement.get('procurementUrgency', '')}", + f"- Budget Range: {requirement.get('budgetRange', '')}", + f"- Risk Level: {requirement.get('riskLevel', '')}", + f"- Current Evaluation Status: {evaluation.get('evaluationStatus', '')}", + f"- Current Recommended Supplier: {recommendation.get('supplier', {}).get('supplierName', '')}", + ] + ) question = "Generate a supplier evaluation for this project." + prompt = f""" -You are an AI supplier evaluation assistant for construction engineering procurement. + You are an AI supplier evaluation assistant for construction engineering procurement. + + Analyze the selected project and supplier data below. Do not ask for more + project details unless the supplied data is actually missing. Produce the + analysis now. -Analyze the selected project and supplier data below. Do not ask for more -project details unless the supplied data is actually missing. Produce the -analysis now. + Industry: + Construction Engineering -Industry: -Construction Engineering + User request: + {question} -User request: -{question} + Selected project profile: + {project_profile_text} -Selected project profile: -{project_profile_text} + Project and supplier JSON: + {json.dumps(project_json, default=str)} -Project and supplier JSON: -{json.dumps(project_json, default=str)} + Available supplier recommendation records: + {available_data_text} -Available supplier recommendation records: -{available_data_text} + Decision rules: + - Use APPROVE when the supplier is a strong fit and material risks are controlled. + - Use REQUEST INFO when inspection logs, capacity confirmation, certificates, + submittals, RFIs, safety records, or schedule evidence are missing. + - Use DENY when the supplier cannot satisfy core technical, compliance, + delivery, or safety requirements. + - For evidence that says documentation is complete and risk is Low, + recommend APPROVE. + - For Harbor Seismic Retrofit, deny the current suppliers and recommend + submitting a new RFP because the supplier pool does not meet DBE, AISC, + NCR, and logistics requirements. + - For North Campus Lab Expansion, treat an uploaded technical addendum PDF + as new evidence and explicitly reflect it in the re-analysis. -Decision rules: -- Use APPROVE when the supplier is a strong fit and material risks are controlled. -- Use REQUEST INFO when inspection logs, capacity confirmation, certificates, - submittals, RFIs, safety records, or schedule evidence are missing. -- Use DENY when the supplier cannot satisfy core technical, compliance, - delivery, or safety requirements. -- For evidence that says documentation is complete and risk is Low, - recommend APPROVE. -- For Harbor Seismic Retrofit, deny the current suppliers and recommend - submitting a new RFP because the supplier pool does not meet DBE, AISC, - NCR, and logistics requirements. -- For North Campus Lab Expansion, treat an uploaded technical addendum PDF - as new evidence and explicitly reflect it in the re-analysis. + Return a concise, decision-ready supplier evaluation with these exact sections: -Return a concise, decision-ready supplier evaluation with these exact sections: + Project Summary + Key Sourcing Requirements + Top 3 Supplier Recommendations + Risks and Missing Information + Actionable Steps + """ -Project Summary -Key Sourcing Requirements -Top 3 Supplier Recommendations -Risks and Missing Information -Actionable Steps -""" + model_id = os.getenv("OCI_MODEL_ID", "meta.llama-3.3-70b-instruct") + config_path = os.path.expanduser(os.getenv("OCI_CONFIG_PATH", "~/.oci/config")) + endpoint = os.getenv("ENDPOINT") + compartment_id = os.getenv("COMPARTMENT_OCID") - print("Generating AI response...") - print(" ") + if not endpoint or not compartment_id: + raise ValueError("Missing ENDPOINT or COMPARTMENT_OCID environment variable.") + + print(f"Generating AI response with model: {model_id}") genai_client = oci.generative_ai_inference.GenerativeAiInferenceClient( - config=oci.config.from_file(os.getenv("OCI_CONFIG_PATH", "~/.oci/config")), - service_endpoint=os.getenv("ENDPOINT") + config=oci.config.from_file(config_path), + service_endpoint=endpoint ) chat_detail = oci.generative_ai_inference.models.ChatDetails( - compartment_id=os.getenv("COMPARTMENT_OCID"), + compartment_id=compartment_id, chat_request=oci.generative_ai_inference.models.GenericChatRequest( - messages=[oci.generative_ai_inference.models.UserMessage( - content=[oci.generative_ai_inference.models.TextContent(text=prompt)] - )], + messages=[ + oci.generative_ai_inference.models.UserMessage( + content=[ + oci.generative_ai_inference.models.TextContent(text=prompt) + ] + ) + ], temperature=0.0, - top_p=1.00 + top_p=1.00, + max_tokens=1500 ), serving_mode=oci.generative_ai_inference.models.OnDemandServingMode( - model_id="meta.llama-3.2-90b-vision-instruct" + model_id=model_id ) ) - chat_response = genai_client.chat(chat_detail) + + try: + chat_response = genai_client.chat(chat_detail) + except ServiceError as error: + if error.status == 404: + raise RuntimeError( + f"Model '{model_id}' is unavailable at this endpoint: {endpoint}" + ) from error + raise + return chat_response.data.chat_response.choices[0].message.content[0].text @@ -429,6 +466,7 @@ Actionable Steps project_json, df_supplier_recommendations ) + print(recommendations) ``` @@ -738,6 +776,11 @@ This step: return cursor.fetchone()[0] + def read_lob(value): + """Convert an Oracle LOB to text; return ordinary values unchanged.""" + return value.read() if isinstance(value, oracledb.LOB) else value + + print("Processing your question using AI Vector Search...") try: @@ -748,17 +791,18 @@ This step: SELECT CHUNK_ID, CHUNK_TEXT FROM CE_PROJECT_CHUNKS WHERE PROJECT_ID = :project_id - AND CHUNK_VECTOR IS NOT NULL + AND CHUNK_VECTOR IS NOT NULL ORDER BY VECTOR_DISTANCE(CHUNK_VECTOR, :qv, COSINE) FETCH FIRST 4 ROWS ONLY """, - {"project_id": selected_project_id, "qv": q_vec} + { + "project_id": selected_project_id, + "qv": q_vec + } ) + retrieved = [ - ( - row[0], - row[1].read() if isinstance(row[1], oracledb.LOB) else row[1] - ) + (row[0], read_lob(row[1])) for row in cursor.fetchall() ] @@ -766,93 +810,144 @@ This step: retrieved = [(0, recommendations)] requirement = (project_json.get("requirements") or [{}])[0] - available_data_text = "\n".join([ - ( - f"Supplier Evaluation {row['EVALUATION_ID']}: " - f"{row['SUPPLIER_NAME']} | Decision: {row['RECOMMENDATION']} | " - f"Fit Score: {row['FIT_SCORE']} | Risk: {row['RISK_LEVEL']} | " - f"Capacity: {row['CAPACITY_STATUS']} | " - f"Explanation: {row['EXPLANATION']} | " - f"Missing Information: {row['MISSING_INFORMATION']}" + + available_data_text = "\n".join( + [ + ( + f"Supplier Evaluation {row['EVALUATION_ID']}: " + f"{row['SUPPLIER_NAME']} | " + f"Decision: {row['RECOMMENDATION']} | " + f"Fit Score: {row['FIT_SCORE']} | " + f"Risk: {row['RISK_LEVEL']} | " + f"Capacity: {row['CAPACITY_STATUS']} | " + f"Explanation: {row['EXPLANATION']} | " + f"Missing Information: {row['MISSING_INFORMATION']}" + ) + for row in df_supplier_recommendations.to_dict(orient="records") + ] + ) + + project_profile_text = "\n".join( + [ + f"- Project Name: {project_json.get('projectName', '')}", + f"- Location: {project_json.get('location', '')}", + f"- Project Phase: {project_json.get('projectPhase', '')}", + f"- Required Trade: {requirement.get('tradeCategory', '')}", + f"- Delivery Window: {requirement.get('deliveryWindow', '')}", + f"- Budget Range: {requirement.get('budgetRange', '')}", + f"- Risk Level: {requirement.get('riskLevel', '')}", + ] + ) + + context_text = "\n========\n".join( + str(chunk_text) for _, chunk_text in retrieved + ) + + rag_prompt = f""" + You are the AI Procurement Guru for construction engineering. + + Use only the supplied project profile, supplier recommendation records, + and retrieved context. Do not invent supplier names. Only use supplier names + that appear verbatim in the supplier recommendation records or retrieved + context. If the evidence is insufficient, say so plainly. + + Keep the answer under 220 words and make it decision-ready. + + Question: + {question} + + Selected Project Profile: + {project_profile_text} + + Available Supplier Recommendation Records: + {available_data_text} + + Retrieved Context: + {context_text} + + Tasks: + 1. Answer the question directly. + 2. Justify the answer using fit, risk, delivery, and documentation signals. + 3. If there is a reasonable backup supplier, name it briefly. + """ + + model_id = os.getenv( + "OCI_MODEL_ID", + "meta.llama-3.3-70b-instruct" + ) + + config_path = os.path.expanduser( + os.getenv("OCI_CONFIG_PATH", "~/.oci/config") + ) + endpoint = os.getenv("ENDPOINT") + compartment_id = os.getenv("COMPARTMENT_OCID") + + if not endpoint or not compartment_id: + raise ValueError( + "Missing ENDPOINT or COMPARTMENT_OCID environment variable." ) - for row in df_supplier_recommendations.to_dict(orient="records") - ]) - project_profile_text = "\n".join([ - f"- Project Name: {project_json.get('projectName', '')}", - f"- Location: {project_json.get('location', '')}", - f"- Project Phase: {project_json.get('projectPhase', '')}", - f"- Required Trade: {requirement.get('tradeCategory', '')}", - f"- Delivery Window: {requirement.get('deliveryWindow', '')}", - f"- Budget Range: {requirement.get('budgetRange', '')}", - f"- Risk Level: {requirement.get('riskLevel', '')}" - ]) - context_text = "\n========\n".join(text for _, text in retrieved) - - rag_prompt = f"""[INST] <> -You are the AI Procurement Guru for construction engineering. -Use only the supplied project profile, supplier recommendation -records, and retrieved context. -Do not invent supplier names. -Only use supplier names that appear verbatim in the supplier -recommendation records or retrieved context. -If the evidence is insufficient, say so plainly. -Keep the answer under 220 words and make it decision-ready. -<> [/INST] -[INST] -Question: "{question}" - -Selected Project Profile: -{project_profile_text} - -Available Supplier Recommendation Records: -{available_data_text} - -Retrieved Context: -{context_text} - -Tasks: -1. Answer the question directly. -2. Justify the answer using fit, risk, delivery, and documentation signals. -3. If there is a reasonable backup supplier, name it briefly. - [/INST]""" - - print("Generating AI response...") + + print(f"Generating AI response with model: {model_id}") genai_client = oci.generative_ai_inference.GenerativeAiInferenceClient( - config=oci.config.from_file(os.getenv("OCI_CONFIG_PATH", "~/.oci/config")), - service_endpoint=os.getenv("ENDPOINT") + config=oci.config.from_file(config_path), + service_endpoint=endpoint ) + chat_detail = oci.generative_ai_inference.models.ChatDetails( - compartment_id=os.getenv("COMPARTMENT_OCID"), + compartment_id=compartment_id, chat_request=oci.generative_ai_inference.models.GenericChatRequest( - messages=[oci.generative_ai_inference.models.UserMessage( - content=[oci.generative_ai_inference.models.TextContent(text=rag_prompt)] - )], + messages=[ + oci.generative_ai_inference.models.UserMessage( + content=[ + oci.generative_ai_inference.models.TextContent( + text=rag_prompt + ) + ] + ) + ], temperature=0.0, - top_p=0.90 + top_p=0.90, + max_tokens=800 ), serving_mode=oci.generative_ai_inference.models.OnDemandServingMode( - model_id="meta.llama-3.2-90b-vision-instruct" + model_id=model_id ) ) - chat_response = genai_client.chat(chat_detail) + + try: + chat_response = genai_client.chat(chat_detail) + except ServiceError as error: + if error.status == 404: + raise RuntimeError( + f"Model '{model_id}' is unavailable at this endpoint: {endpoint}\n" + "Select an available model in OCI Generative AI Chat Playground " + "for the same region, then run:\n" + "os.environ['OCI_MODEL_ID'] = 'your-model-id'" + ) from error + raise + ai_response = ( chat_response.data.chat_response.choices[0] - .message.content[0].text + .message.content[0] + .text ) - print("\\n🤖 AI Procurement Guru Response:") + print("\n🤖 AI Procurement Guru Response:") print(ai_response) - print("\\n📑 Retrieved Chunks Used in Response:") - for cid, text in retrieved: - preview = text[:140].replace("\\n", " ") - if len(text) > 140: + print("\n📑 Retrieved Chunks Used in Response:") + for chunk_id, chunk_text in retrieved: + chunk_text = str(chunk_text) + preview = chunk_text[:140].replace("\n", " ") + + if len(chunk_text) > 140: preview += "..." - print(f"[Chunk {cid}] : {preview}") - except Exception as e: - print(f"RAG flow error: {e}") + print(f"[Chunk {chunk_id}]: {preview}") + + except Exception as error: + print(f"RAG flow error: {error}") ```